java基础知识

Java

学前知识

计算机组成

image-20220623183355214.

windows系统

  • Win + E:打开文件资源管理器

  • F2:重命名

二进制基础

健壮性.

JDK 与 JRE

JDK (Java Development Kit)Java开发工具包

JDK=JRE + java的开发工具(java, javac, javadoc, javap等)

JRE (Java Runtime Environment) Java运行环境

JRE = JVM(Java Virtual Machine) + Java和核心类库

如果只想运行 .class 文件,只需要JRE


+号的使用

程序中 + 号的使用

  1. 运算顺序:从左到右 即左-->右

  2. 当 + 左右两边都是数值型时,做加法运算,运算结果还是数值型。

  3. 当 + 左右两边有一方为字符串,则做拼接运算,运算结果成为字符串。

    • 有一方为字符串:说明的是 + 两边任何一边为字符串,都会做拼接运算
    • 如果两边都是字符串的话,那更好,直接做拼接运算

eg. 1 + 2 + "4" = 34 // 先加法 再拼接

eg. "1" + 0+2+ "4" = 1024 // 拼接 拼接 拼接 从左往右计算

eg ."1" + (0+2) + "4" = 124 括号的运算符级别很高 // 加法 拼接 拼接


浮点数比较

小数都是近似值

  1. 当我们对运算结果是小数进行相等判断时,要小心,应该是以两个数差的绝对值在某个精度范围内判断

这里不能直接用 == 来判断,人和计算机是不一样的,计算机的规则就是,计算得到的浮点数就是近似数(即使是正好整除得到的浮点数也是近似数)。如果直接判断则结果一定为false. 所以需要上述的方法。

  1. 当我们直接来比较 浮点数(此时浮点数不是运算结果) 和 整数 是否相等时,不用绝对值,而是直接用 == 来判断。

    int i = 10; double d = 10.0;
    System.out.println(i == d); // true,这里不要认为数据类型不一样就不相等
    

Math.abs(x):求 x 的绝对值

double n1 = 2.7;
double n2 = 8.1/3; // n2 是一个接近2.7的小数,但不是2.7

if (n1 == n2) { // false
    System.out.println("相等~~");
}
if (Math.abs(n1 - n2) < 0.0001) { // true
    System.out.println("相等~~"); // 差值非常小,到我的规定精度,认为相等
}

byte b = 10

难道你没有想过: 10 默认是 int 类型,那为什么赋给 byte 类型的 b 不会报错呢?

这是人家规定的:

当把具体的数值赋给 byte 时,

  1. 先判断该数是否在 byte 范围内(-128~127),如果是就可以 。

  2. 如果是变量赋值,那么就要先判断类型

    eg. byte b1 = 10; // ok -128~127 具体数赋值

    ​ byte b3 = b1; // ok 变量赋值,都是byte 类型

    eg. int n = 10; // n 是 int

    ​ byte b2 = n; // 错误,变量赋值,判断类型 int 不能自动转成 byte

    ​ char c = b1; // 错误,byte 不能自动转成 char


1.0*a/b 与 a/b*1.0

a,b 均为 int 类型(int a, int b)

1.0*a/b:先运算1.0*a,这是运算变成了double类型,得出的结果就很正常了(我们所需要的结果

a/b*1.0:先运算a/b,若a < b 则结果为0,a/b 在结果为先取整在变浮点数,1.0没有起到该起到的作用,why?

因为忘了最简单乘除是同级运算,按从左到右计算!!!(即使把这个结果赋给一个double类型,结果也不是我们想要的)

结论:要想得到的结果为 正确的浮点数 那么必须要在 / 的左边 乘以 1.0,来转化为浮点数


byte short char 三者计算

byte short char 他们三者可以计算,在计算时首先转换为 int 类型,首先,首先!!

eg. byte b1 = 1;

byte b2 = 2;

short s1 = 3;

char c = 4;

short s2 = b1 + s1 // 错误, b1+s1 => int

int i = b1+s1+c; // ok b1+s1+c => int

byte b3 = b1+b2; // 错误 b1+b2 => int


基本数据类型 (4类8种)--> String类型 ( "" )

语法:基本数据类型+""(空串,空的字符串)

注意:" "空格不是空的字符串

int i = 100;

int 类型 --> String 类型

String s = i + "";

char c = '软';

char类型-->String类型

String s2 = c + "";


String类型 --> 基本数据类型 ( Xx.parseXx() )

注意:再将String类型转成基本数据类型时,要确保String类型能够转成有效的数据

eg.我们可以把 "123" ,转成一个整数,但是不能把 "hello" 转成一个整数;如果格式不正确,就会抛出异常,终止程序

语法:通过基本数据类型的包装类调用parseXXX方法即可,参数为字符串类型

整型:

byte b = Byte.parseByte("12")

short s = Short.parseShort("12");

int i = **Integer.parseInt("123"); ** 参数为字符串类型

long l = Long.parseLong("123");

浮点型:

float f = Float.parseFloat("123.45");

double d = Double.parseDouble("123.45");

布尔型:

boolean o = Boolean.parseBoolean("true");


String字符串 --> char字符 charAt(0)

含义:把字符串的第一个字符得到

char c = "Char".charAt(0); 得到 "Char" 字符串的第一个字符 'C'


%的本质

a是整数:a % b = a - a/b*b;

10%3 = 10 - 10/3 * 3 = 10 - 3 * 3 = 1

a是小数:a % b = a - (int)a/b*b;

-10.5 % 3 = -10.5 - (int)(-10.5)/3*3 = -10.5 - (-10)/3 * 3 = -10.5 + 9 = -1.5


强转转化 ()

int a = (int)1.5; // 正确 1 采用小数点后抹去的方式取整

int b = (int)(-2.5); // 正确 -2 采用小数点后抹去的方式取整

int c = int (3); // 错误 需要将 int 用 () 括起来,而不是将3 括起来,这样写是错误的

int d = (int)"18"; // 错误,字符串转成int,应用相应的包装类 int d = Integer.parseInt("18");


i=i++

使用临时变量来分析

int i = 2;
i = i++;
// 等价于
int temp = i; // 等号右边的i
i = i + 1; // 两个i都是等号右边的i
i = temp; // 等号左边的i

System.out.println("i="+i); // i=2

int i = 3;
a = i++ + 5;
//等价于
temp = i + 5;// temp = 8
i = i + 1; // i = 4
a = temp; // a = 8

i=++i

使用临时变量来分析

int i = 2;
i = ++i;
//d
i = i + 1; // 都是等号右边的i
int temp = i; // 等号右边的i
i = temp; // 等号左边的i

System.out.println("i="+i); // i=3

赋值运算符 以及 b += 2;

赋值运算符的左边只能是变量,右边可以是变量表达式常量值.

int a = b;// 变量:a; 表达式:b + 2; 常量值:32;

复合赋值运算符会进行类型转换

byte b = 3;     
b += 2; <=等价=> b = (byte)(b+2); 不等价于 b = b + 2;(这样会直接报错 int -> byte)

位移运算符

  • 左乘右除

<< 左移: 左移几位就是上几个2.

>> 右移: 右移几位就是除以(整除)几个2.

  • &

三目运算符

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

一真大师 当条件表示为true -->执行表达式1;false --> 执行表达式2

注意: 第一个符号为问号 第二个符号为冒号 : 第三个符号为分号 ;

问号 冒号 分号

// 注意:(3条)
// (1)表达式1 与表达式2 最后的类型必须得有返回结果,即不能为是 void,若为 void ,编译时将会报错。
// (2)表达式1 与表达式2 不会被同时执行,两者只有一个会被执行。
// (3)表达式1 与表达式2 可以是常量,变量,表达式,方法调用,但却只能有一条语句.

// The Java Language Specification(简称:JLS)Java语言规范(3点)
// (1)用大白话讲,如果表达式1 与表达式2 类型相同,那么这个不用任何转换,三目运算符表达式结果当然与表达式 1,2 类型一致。
// (2)当表达1 或表达式2 其中任一一个是基本数据类型,比如 int,而另一个表达式类型为包装类型,比如 Integer,那么三目运算符表达式结果类型将会为基本数据类型,即 int。 即Java 编译器在编译过程加入自动拆箱机制。
// (3)大白话讲,当表达式1 与表达式2 类型不一致,但是都为数字类型时,低范围类型将会自动转为高范围数据类型,即向上转型。这个过程将会发生自动拆箱。
boolean flag = true; //设置成true,保证表达式1 被执行
int simpleInt = 66;
Integer nullInteger = null;

int result = flag ? nullInteger : simpleInt;
// 上式等价于 int result = flag ? nullInteger.intValue() : simpleInt;
// z
boolean flag = true; //设置成true,保证表达式 2 被执行
int simpleInt = 66;
Integer nullInteger = null;
Integer objInteger = Integer.valueOf(88);

int result = flag ? nullInteger : objInteger;
// 上式等价于 int result = (flag ? nullInteger : objInteger).intValue;
boolean flag = true; //设置成true,保证表达式 2 被执行
int simpleInt = 66;
Integer nullInteger = null;

Integer result = flag ? nullInteger : simpleInt;
// 上式等价于 Integer result = Integer.valueOf(flag ? nullInteger.intValue() : simpleInt);
boolean flag = true; //设置成true,保证表达式 2 被执行
Integer nullInteger = null;
Long objLong = Long.valueOf(88l);

Object result = flag ? nullInteger : objLong;
// 第一步,将 nullInteger拆箱。nullInteger.intValue();
// 第二步,将上一步的值转为 long 类型,即 (long)nullInteger.intValue()。
// 第三步,由于表达式1 变成了基本数据类型,表达式2 为包装类型,根据案例 1 讲到的规则,包装类型需要转为基本数据类型,所以表达式2 发生了拆箱。objLong.longValue();
// 第四步,由于三目运算符最后的结果类型为基本数据类型:long,但是左边类型为 Object,这里就需要把 long 类型装箱转为包装类型。
// 上式等价于 Object result = Long.valueOf(flag ? (long)nullInteger.intValue() : objLong.longValue());

运算符优先级

成员访问 . 与 强转 () : 先成员访问,然后再进行强转.

成员访问 . 与 取反 ! : 先成员访问,然后再进行取反.

image-20220108100333183.


Scanner

  1. 入该类的所在import java.util.Scanner;
  2. 创建该类对象声明变量),Scanner myScanner = new Scanner(System.in);
  3. 调用里面的功能(方法):
    • String name = myScanner.next(); // 接收用户输入的字符串.
    • int age = myScanner.nextInt(); // 接收用户输入的int.
    • double money = myScanner.nextDouble(); // 接收用户输入的 double.
    • char gender = myScanner.next().charAt(0); // 接收用户输入的字符 ,本质就是取字符串的第一个字符.

HEX(hex) 十六进制

DEC(dec) 十进制

OCT(oct) 八进制

BIN(bin) 二进制

2、8、16进制 ==> 10进制

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以 2的(位数-1)次方,然后求和

但是代码却是从最高位开始,但却并不违背上面的规则。下面的转进制也不违背,反而更巧妙.

String str = 1101
int sum = 0;

for (int i = 0; i < str.length(); i++) {
    // 二进制
    sum = sum * 2 + (str.charAt(i) - '0'); 
}
/**
i的值  ==> 原sum ;                   改变后的sum
i = 0 ==> sum = 0;              sum = 0 * 2 + 1;
i = 1 ==> sum = 1;              sum = 1 * 2 + 1;
i = 2 ==> sum = 1*2+1;          sum = (1*2+1)*2 + 0 = 1*2^2 + 1*2 + 0
i = 3 ==> sum = 1*2^2 + 1*2 +0; sum = (1*2^2 + 1*2 + 0)*2+1 = 1*2^3 + 1*2^2 +0*2 + 1

最终的sum: sum = 1*2^3 + 1*2^2 +0*2 + 1; 就问你妙不妙!!
*/

注意:

  1. str.charAt(i)字符类型,而字符类型的 '1' 对应十进制的 49,所以要先减去 '0' 来统一 大小

    str.charAt(i) - '0' == 对应十进制的 0-9

  2. sum = sum * 2 + (str.charAt(i) - '0'); 非常巧妙,从最高位开始累计,最高位正好是 n-1 次方

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以 8的(位数-1)次方,然后求和

String str = 1024
int sum = 0;

for (int i = 0; i < str.length(); i++) {
    // 八进制
    sum = sum * 8 + (str.charAt(i) - '0'); 
}

注意:同上

规则:从最低位(右边)开始,将每个位上的数提取出来,乘以 16的(位数-1)次方,然后求和

String str = ABC
int sum = 0;

for (int i = 0; i < str.length(); i++) {
    // 十六进制
    if (str.charAt(i) >= 'a' && str.charAt(i) <= 'f') {
        sum = sum * 16 + (str.charAt(i) - 'a') + 10;
    } else if (str.charAt(i) >= 'A' && str.charAt(i) <= 'F') {
        sum = sum * 16 + (str.charAt(i) - 'A') + 10;
    } else if (str.charAt(i) >= '0' && str.charAt(i) <= '9') {
        sum = sum * 16 + (str.charAt(i) - '0');
    }
}

注意:

  1. 由于十六进制的 10-15A-F (a-f) 来表示因此需要判断,str.charAt(i) 究竟是什么。
  2. (str.charAt(i) - 'a') + 10 来表示 10-15,别忘了要加上10

10进制 ==> 2、8、16进制(本质:栈结构)

规则:将该数不断除以2(进制数),直到商为0为止,然后将每步得到的余数倒过来,就是对应的二进制(相应进制)。

int num = 34;
int flag = 2; // 2、8、16进制的运算是一样的,只是在遍历的时候有区别
int[] arr = new int[20];
int len = 0; // 记录数组中有效数据的长度

// 方法一:使用数组
do {
    arr[len] = num%flag;
    len++;
    num = num / flag;
} while (num > 0);
// 遍历数组,从后往前遍历输出
for (int i = len -1; i >= 0 ; i--) {
    System.out.print(arr[i]);
}
// 十六进制的遍历,从后往前遍历输出
for (int i = len -1; i >= 0 ; i--) {
    if (arr[i] >= 10 && arr[i] <= 15) {
        System.out.print((char)('A' + arr[i]-10)); // 需要强转,否则输出int类型
    } else {
        System.out.print(arr[i]);
    }
}

// 方法二:使用递归
void toBin(int n) {
    if (n > 0) {
        toBin(n/2);
        System.out.print(n%2);
    }
}
// 总结: 十进制转其他进制本质就是:实现栈结构,先进后出!
// 实现:可以利用数组反向遍历,或者 使用递归 实现栈

原码、反码、补码

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

Switch

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

Math.random() ==> [0, 1) 前开后闭

生成随机数方法:Math.random();

随机生成1-100的一个整数(包括1和100):(int)(Math.random() * 100) + 1


break、continue、return

break: 终止某个语句块的执行,终止switch或循环[for while do...while],

注意if 不用 break 终止.

continue: 结束本次循环,继续执行下一次循环.

return: 终止所在方法.


for循环与while、do...while循环的循环迭代语句的区别

注意for循环的循环迭代语句 和 while循环、do...while循环不一样.

  1. while循环、do...while循环 中的continue 会影响 循环迭代语句的执行。
  2. for循环中的continue 不会影响 循环迭代语句的执行。
while、do...while循环
// while、do...while循环 的 循环迭代语句紧跟着循环体,因此如果循环体不能完全执行,
// 例如使用continue语句来结束本次循环,则循环迭代语句不会被执行。(可能会发生问题,eg.死循环)
@Test
public void m1() {
    int count = 1;
    while (count < 5) {
        System.out.println(count);
        // 解读一下:
		// 当程序中的count 增加到3 时,会执行if语句->continue, 跳过本次循环
        // 语句count++,被跳过,执行下一次循环时count依旧还是 3,继续执行if语句,跳过本次循环
        // 当count的值到达3后,将一直输出3,形成死循环
        if (count == 3) {
            continue;
        }
        count++;
    }
}
// 上面程序输出结果:1、2、3、3、3、3...(死循环)
// 解决死循环方案
// 方案1:在if语句的 continue之前增加count++语句(有点繁琐)
@Test
public void m1() {
    int count = 1;
    while (count < 5) {
        System.out.println(count);
        if (count == 3) {
            count++;// 增加count++语句,防止死循环
            continue;
        }
        count++;
    }
}
// 方案2:在if判断中利用count++ == 3 替换 count==3
// 简洁,但是需要一点点的理解
@Test
public void m1() {
    int count = 1;
    while (count < 5) {
        System.out.println(count);
        // 利用count++ == 3 替换 count==3
        // count++ == 3
        // 1.++在count 的后面,因此先进行条件判断(是否和3相等),再执行count++
        // 2.此时的count++,在continue 之前,可以防止死循环。
        // 3.每次都会执行if条件判断,实现count++,作为循环的迭代语句
        if (count++ == 3) {
            continue;
        }
        System.out.println("lemon");
    }
}
// 方案3:可以将count++ 语句 放在 if语句的前面吗??? 不可以,得不到正确的结果
@Test
public void m1() {
    int count = 1;
    while (count < 5) {
        System.out.println(count);
        // 当count == 2时,count++ == 3,因此会在count == 2时执行下面的if语句,
        // 但其实我们希望的下一次循环在执行if语句
        count++;// 这样写会出问题(输出不正确)
        if (count== 3) {
            continue;
        }
        System.out.println("lemon");
    }
}
for循环
// for循环的循环迭代语句 并没有与循环体放在一起,因此不管 是否使用continue语句来结束本次循环,
// 循环迭代语句一样会得到执行
@Test
public void m2() {
    for (int i = 0; i < 5; i++) {
        System.out.println(i);
        if (i == 3) {
            continue;// for循环中的continue 不会影响循环迭代语句的执行
        }
        System.out.println("lemon");
    }
}

数组

java数组是静态的

数组被初始化之后,数组所占的内存空间,数组的长度都是不可变的,因此说Java数组是静态的


length 属性

arr.length:访问数组的长度

注意:这是数组的属性,而不是方法。


数组3种初始化


数组的2种静态初始化

  1. String[] strArr1 = new String() {"静态", "初始", "化"};
image-20211104213045266
  1. String[] strArr2 = {"静态", "初始", "化"};
image-20211104213331391

注意:这两种静态初始化都有大括号 {} 和 **最后面的分后 ; **

数组的1种动态初始化

  1. String[] strArr3 = new String[3];
image-20211104213523689

三种初始化在内存中显示

image-20211104214853591

strArr1 strArr2 strArr3 的指向

image-20211104225742092

strArr1 = strArr2; 将strArr2的地址赋给strArr1,那么strArr1就和strArr2的指向一样了,即strArr1也指向了strArr2所指向的堆中的那块内存空间。

strArr3 = strArr2; ** 将strArr2的地址赋给strArr3,那么strArr3就和strArr2的指向一样**了,即strArr3也指向了strArr2所指向的堆中的那块内存空间。

即 strArr1 strArr2 strArr3 指向相同


数组一定要初始化吗?

始终记住:java的数组变量只是引用类型的变量,它并不是数组对象本身,只要让数组变量指向有效的数组变量,程序中即可使用该数组变量。

对数组执行初始化,其实并不是对数组变量执行初始化,而是在堆内存中创建数组对象——也就是为该数组对象分配一块连续内存空间,这块连续内存空间的长度就是数组的长度。

数组变量(引用变量不需要所谓的初始化操作)

数组变量只是一个引用变量(有点类似于C语言的指针),想象成一个 " 杯子 "。

数组对象(引用变量所引用的对象,需要初始化操作)

数组对象就是保存在堆内存中的连续内存空间

二维数组(数组的数组)

二维数组在内存的示意图

  1. int[][] arr = new int[3][2];
image-20211106152136976
// 二维数组的遍历,两层循环
for (int i = 0; i < arr.length; i++) {
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + " ");
    }
    System.out.println();
} 
/*
0 0
0 0
0 0
*/ 

提示:对于很多java程序员而言,他们最容易混淆的是:

引用类型的变量何时只是栈内存中的变量本身,何时又变为引用实际的java对象

其实规则很简单:

引用变量本质上只是一个指针,只要程序通过引用变量访问属性(arr.length),或者通过引用变量来调用方法,该引用变量就会由它所引用的对象代替

eg. arr 本质上只是 main 栈区的引用变量,但使用 arr.length、arr[2] 时,系统将会自动变为访问堆内存中的数组对象

  1. int[][] arr = int[3][];
image-20211106152536651
for (int i = 0; i < arr.length; i++) {
    System.out.print(arr[i] + " ");
}
/*
null null null
*/

为arr[0] 和 arr[2]赋值

image-20211106153524969
// 一开始数组中的元素都执行默认初始化
for (int i = 0; i<arr.length; i++) {
    if (i == 1) {
        continue;
    } else {
        arr[i] = new int[i+1];
    }
}
// arr[1].length 会报空指针异常 ,因为arr[1] = null,因此会报错
// 数组中length是一个属性,不是方法!!!

类与对象

类:自定义的数据类型(本质就是 数据类型)

int:java提供的数据类型

细节:

  1. 类的定义class Xxx{} 或 public class Xxx{}.

    接口注意事项和细节第9条.

  2. 类 = 属性 + 成员方法

注意

  1. Xxx的后面不能加上 ()压根就不能这样定义类定义方法/使用方法时需要 () 形参列表
  2. 类中的 功能/行为 要写在方法中,而不能写在属性和方法之间
  3. 功能语句 写在方法中。!!!切记,切记
class Person {
    String name;
    int age;
    // 下面是正确的
    public void say() {
        System.out.println(name + " : " + age);
    }
    // 下面完全错误
    System.out.println(name + " : " + age);// 功能语句写在方法中
    public void say() {
        
    }
}

对象:就是一个类的具体实例

对象在内存中的存在形式(重要的)必须搞清楚

java内存结构:

  1. :一般存放基本数据类型(局部变量)
  2. 存放对象(new Cat() , 数组等)
  3. 方法区常量池常量, 比如字符串),类加载信息

java创建对象的流程:

Cat cat1 = new Cat();
cat1.name = "小白";
cat1.age = 12;
cat.color = "紫色";
  1. 先加载Cat类信息属性和方法信息只会加载一次
  2. 在堆中分配空间,进行默认初始化(看规则, 下面)
  3. 把地址赋给 cat1,cat1 就指向对象
  4. 进行指定初始化, 比如 cat1.name = "小白"; cat1.age = 12; cat.color = "紫色";
image-20211108233635696

默认值初始化规则

byte short int long --> 0

float double --> 0.0

char --> '\u0000'

boolean --> false;

String --> null;

属性/成员变量/字段(field)/实例变量/普通属性/普通变量/非静态变量

细节:

  1. 属性有默认值,规则同数组一样

成员方法(方法)

成员方法传参机制 -> 基本数据类型 (四类八种)

结论:对于基本数据类型,传递的是值(值拷贝),形参 的任何改变不影响 实参!!

Parameter 参数.

public class MethodParameter {

	public static void main(String[] args) {
		int a = 10;
		int b = 20;

		AA obj = new AA();
		obj.swap(a, b);
		System.out.println("main方法 a=" + a + " b=" + b); // a=10 b=20

	}
}

class AA {
    
	public void swap(int a, int b) {
		System.out.println("a和b交换之前a=" + a + " b=" + b);// a=10 b=20
		int tmp = a;
		a = b;
		b = tmp;
		System.out.println("a和b交换之后a=" + a + " b=" + b);// a=20 b=10
	}

}

示意图: swap栈 与 main栈 是2个独立的数据空间

image-20211109203048611

成员方法传参机制 -> 引用数据类型 (数组、对象)

画图注意事项:

  1. 方法的形参列表属于 对应方法栈的 变量,也要在对应的栈中画出来,千万不能忘了
  2. 这要求我们 第一步先把形参变量 写出来,不然很容易忘
  3. 基本数据类型 -> 形参列表 赋值 数值
  4. 引用数据类型 -> 形参列表 赋值 地址。

结论:引用类型传递的是地址(传递也是值,但是值是地址),可以通过 形参 影响 实参

数组示例

public class MethodParameter02 {

	public static void main(String[] args) {
		
		int[] arr = {1, 2, 3};

		B b = new B();
		b.test100(arr);
        // 遍历数组
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();// 200, 2, 3
	}
}

class B {

	public void test100(int[] arr) {

		arr[0] = 200;
		// 遍历数组
		for (int i = 0; i < arr.length; i++) {
			System.out.print(arr[i] + " ");
		}
		System.out.println();// 200, 2, 3
	}
}

示意图:

image-20211111205541746

对象示例

public class MethodParameter02 {

	public static void main(String[] args) {
	
		Person p = new Person();
		p.name = "jack";
		p.age = 5;

		B b = new B();
		b.test200(p);

		System.out.println("main方法中 p.age=" + p.age);// 100

	}
}

class Person {
	String name;
	int age;
}

class B {

	public void test200(Person p) { // 把对象的地址赋给了变量p
		p.age = 100;
		System.out.println("test200方法中 p.age=" + p.age);// 100
	}
}

示意图:

image-20211111211008078

递归

注意

  1. 执行一个方法时,就创建一个新的受保护独立空间(栈空间)
  2. 方法的局部变量是独立的不会相互影响,比如下面的n变量。
  3. 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据
  4. 递归必须有出口,否则就会出现(StackOverflowErroe, 死龟了)。

示例:

Recursion 递归.

public class Recursion {

	public static void main(String[] args) {

		T t1 = new T();
		t1.test(4);
	}
}

class T {

	public void test(int n) {
		if (n > 2) {
			test(n - 1);
		}
		System.out.println("n=" + n);
    }
}

示意图:

image-20211111220353034

8皇后问题

技巧:

  1. Math.abs(n-i) == Math.abs(array[n] - array[i]): 表示判断第n个皇后是否和第i皇后是否在同一斜线包括了正斜线和反斜线,这点非常巧妙,形如 y = kx
  2. arr[i] == arr[n]:判断是否在同一列
  3. 因为 行是递增的,肯定行不相等,所以不需要判断
// 检查是否在同一列,同一斜线上
public boolean check(int[] arr, int n) {
    for (int i = 0; i < n; i++) { // 注意条件是 n
        if (arr[i] == arr[n] || Math.abs(n-i) == Math.abs(arr[n] - arr[i])) {
            return false;
        }
    }
    return true;
}
// 找8皇后的位置
public void findQueue(int[] arr, int n) {
    // 递归结束条件
    if (n == 8) {
        // 打印出8皇后的答案
        for (int i = 0; i < arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
        return;
    } else {
        for (int i = 0; i < arr.length; i++) {
            // 假定arr[n] 的值就是 i
            arr[n] = i;
            if (check(arr, n)) { // 判断上面的假定是否是正确的
                findQueue(arr, n+1);
            }
        }
    }
}

方法重载 OverLoad

细节

  1. 方法名必须相同
  2. 形参列表必须不同(形参个数类型顺序,三者至少有一个不同参数名无要求)。
  3. 返回类型无要求

可变参数

细节:

/*
 * 访问修饰符 返回类型 方法名(数据类型... 形参名) {
 * 
 * }
 */
public void varParameter(int... nums) {
    
}
  1. 可变参数的实参可以为 0 个或者 任意多个
  2. 可变参数的实参可以为 数组
  3. 可变参数的本质就是数组
  4. 一个形参列表中只能出现一个可变参数
  5. 可变参数必须只能在最后

作用域 Scope

细节:

  1. 在java编程中,主要的变量就是属性(成员变量)和局部变量
  2. 局部变量:一般指 在成员方法中定义的变量。(注意是一般幺)
  3. java中作用域的分类:
    • 全局变量:也就是属性,作用域为整个类体
    • 局部变量:也就是除了属性之外的其他变量,作用域为定义它的代码块中
  4. 全局变量可以不赋值,直接使用,因为有默认值,局部变量必须赋值后,才能使用,因为没有默认值。

注意1:

  1. 属性和局部变量可以重名,访问时遵循就近原则。(使用 this 和 直接访问 区分)
  2. 在同一个作用域中比如在同一个成员方法中,两个局部变量,不能重名。(两个属性也不能重名
  3. 属性生命周期长,伴随着对象的创建而创建,伴随着对象的销毁而销毁。
  4. 局部变量生命周期短,伴随着它的代码块的执行而创建,伴随着代码块的结束而销毁,即在一次方法调用过程中。
  5. 作用域不同:
    • 全局变量可以被本类使用,或其他类使用(通过对象调用)。
    • 局部变量只能本类对应的方法中使用。
  6. 修饰符不同:
    • 全局变量/属性 可以加修饰符。
    • 局部变量 可以加修饰符。

构造器 (constructor)/构造方法

作用:

  • 完成对新对象的初始化并不是创建对象

基本语法:

  • [ 修饰符 ] 方法名(形参列表) { 方法体; }

说明:

  • 构造器的修饰符可以默认
  • 构造器没有返回值void 也不能写。
  • 方法名 和 类名 必须一样。
  • 形参列表 和 成员方法一样的规则。
  • 构造器的调用由系统完成
  • 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器(也叫默认构造器),可用 javap 指令查看。
  • 一旦定义了自己的构造器,默认的构造器就被覆盖了,就不能再使用默认的无参构造器,除非显式的定义一下。(这非常重要)!!

对象创建流程

class Person {
    int age = 90;
    String name;
    Person(String n, int a) {
        name = n;
        age = a;
    }
}
Person p = new Person("小倩", 20);

image-20211117202144389.

  • 流程分析( 面试题 ):
    1. 加载Person类信息(Person.class),只会加载一次.
    2. 在堆中分配空间(地址).
    3. 完成对象初始化.
      • 3.1 默认初始化 age=0, name=null,
      • 3.2 显式初始化 age=90, name=null
      • 3.3 构造器的初始化 age=20, name="小倩".
    4. 把对象在堆中的地址,返回给 p (p 是对象名,也可以理解成对象的引用).

this 关键字

什么时候用 this 关键字? 答:当 属性 与 局部变量 重名需要区分时用 this.属性名 表示 对象的属性

class Dog {
    String name;
    int age;
    
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void info() {
        System.out.println(this.name + "\t" + this.age + "\t" + "当前对象的hashCode是:" + this.hashCode());
    }
}

Dog dog1 = new Dog("大壮", 3);
dog1.info();
Dog dog2 = new Dog("大黄", 2);
dog2.info();
   

示意图:

image-20211117205138948
  • this:表示当前运行类型对象
  • 哪个运行类型对象调用,this 就代表哪个运行类型对象.

细节:

  1. this 关键字可以用来访问本类属性方法构造器,但是不能访问局部变量
  2. this 用来区分当前类的属性和局部变量
  3. 访问成员方法的语法:this.方法名(形参列表);
  4. 访问构造器语法:this(形参列表);
    • 只能在构造器中使用(即只能在构造器中访问另外一个构造器)
    • 必须放在第一条语句
  5. this 不能类定义的外部使用只能在类定义的方法中使用
  6. this: 先访问本类的属性,如果没有,则去父类找本类属性->父类属性.

str1.equals(str2)

字符串比较用 equals() 方法,不能用 == 比较

匿名对象

new Test();// 匿名对象,匿名对象只能使用一次,使用后就不能再使用了

IDEA

快捷键 [必须熟练使用的]

  1. 删除当前行 ctrl + d
  2. 复制当前行 ctrl + alt + 向下光标
  3. 补全代码 alt + /
  4. 添加注释和取消注释 ctrl + /
  5. 导入该行需要的类,先配置auto import,然后使用 alt + enter 即可
  6. 快速格式化代码 ctrl + alt + L
  7. 快速运行程序 alt + R
  8. 生成构造器 alt + insert [提高开发效率]
  9. 查看一个类的层级关系 ctrl + H [学到继承后,非常有用]
  10. 将光标放在一个方法上,输入 ctrl + B, 可以定位到方法 [学到继承后,非常有用]
  11. 自动分配变量名, 通过在后面加 .var
  12. 新建java类 ctrl + N
  13. 搜索 ctrl + F
  14. 修改名称 shift + F6

IDEA模板/自定义模板

file ==》 settings ==》editor ==》Live Templates ==》查看有哪些模板快捷键/可以自己增加模板.

evaluate 评价.

appearance 外观.

template 模板.

包的三大作用

  1. 区分相同名字的类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围

包的基本语法

package com.lemon;
说明:
    1. package 关键字,表示打包
    2. com.lemon:表示包名

包的本质

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

image-20211119204741281

包的命名

命名规则:只能包含数字、字母、下划线、小圆点,但不能用数字开头不能时关键字或保留字

命名规范:

  1. 一般是小写字母 + 小圆点
  2. com.公司名.项目名.业务模块名
com.sina.crm.user // 用户模块
com.sina.crm.order // 订单模块
com.sina.crm.utils // 工具类

常用的包

一个包下,包含很多类,java中常用的包有:

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

如何引入包

语法:import 包;

引入包的主要目的 就是使用该包下的类

import java.util.Scanner;
import java.util.Arrays;

包的细节

  1. package作用声明当前类所在的包需要放在类的最上面一个类中最多只有一句package
  2. import 指令 位置放在 package 的下面在类定义前面可以有多句且没有顺序要求

访问修饰符 (modifier)

java 提供四种访问控制修饰符号,用于修饰方法和属性(成员变量)访问权限(范围)

4 种访问修饰符的访问范围:(背过)

1 访问级别 访问控制修饰符 同类(本类) 同包 子类 不同包
2 公开 public
3 受保护 protected ×
4 默认 没有修饰符 × ×
5 私有 private × × ×

细节:

  1. 修饰符 可以用来修饰类中的属性成员方法及其
  2. 只有 默认public 才能修饰类!,并且遵循上述访问权限的特点。
  3. 成员方法访问规则和属性完全一样

面向对象三大特征

封装 (encapsulation)

封装:就是把抽象出来的数据 (属性) 和对数据的操作 (方法) 封装在一起数据被保护在内部,程序的其他部分只能通过被授权的操作(方法)才能对数据进行操作

好处

  1. 隐藏实现细节 方法(连接数据库) <-- 调用(传入参数...)
  2. 可以对数据进行验证保证安全合理

封装的实现步骤

  1. 属性进行私有化(private)不能直接修改属性修改 和 获取都不可以

  2. 提供一个共有的(public) set方法,用于对属性判断并赋值.

    • public void setXxx(类型 参数名) { // Xxx表示某个属性
          // 加入数据验证的业务逻辑
          this.属性 = 参数名;
      }
      
  3. 提供一个共有的(public) get方法,用于获取属性的值【私有化后不能直接访问属性,只能通过方法】

    • public 数据类型 getXxx() { 
          // 加入权限判断
          return xx;
      }
      

封装与构造器

构造器 与 setXxx 结合,来 保证对数据正确的校验

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

Account 账号 账户

salary 薪水

balance 余额

继承 (inherit)

基本介绍:

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

注意:这里的父类是个总称包括子类以上的所有父类,比如 直接父类:爸爸类,间接父类:爷爷类,祖宗类,这些都称为父类

示意图

image-20211123090955344

基本语法

class 子类 extends 父类 {
}
  1. 子类就会自动拥有父类定义的属性和方法
  2. 父类又叫 超类基类
  3. 子类又叫派生类

improve_ 改进,改善 (开发小技巧,担心它是关键字就加一个下划线 _ )

优点

  1. 代码的复用性提高了。
  2. 代码的扩展性维护性 提高了。

细节

  1. 子类继承了所有的属性和方法非私有的属性和方法可以在子类直接访问,但是私有属性和方法不能在子类直接访问,要通过父类提供公共的方法访问
    • (虽然子类继承了父类的所有的属性和方法 (包括私有的属性和方法) 【用 debug查看】,但是这和子类 能不能 调用父类的所有属性和方法没有关系,这个要具体看访问修饰符)

call 调用.

  1. 子类必须 调用 父类的构造器完成父类的初始化

  2. 创建子类对象时,不管使用子类的哪个构造器默认情况总会调用父类的无参构造器如果父类没有提供无参构造器,则必须在子类的构造器中super指定使用父类的哪个构造器完成对父类的初始化工作否则,编译不会通过。(需要理解)

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

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

  5. 构造器 默认有 super(), 但是不默认有this().

  6. super()this() 都只能放在构造器第一行,因此这两个方法不能共存在一个构造器

    class A {// 父类
        A() {// 父类无参构造器
            System.out.println("a");
        }
        A(String name) {// 父类的有参构造器
            System.out.println("a name");
        }
    }
    class B extends A {// 子类
        B() { // 子类无参构造器
            // B的这个无参构造器中没有 super(); 因为:this() 和 super() 只能出现一个。
            this("abc");
            System.out.println("b");
        }
        B(String name) { // 子类有参构造器
            // 默认有super();
            System.out.println("b name")
        }
    }
    main 中:B b = new B();
    输出结果为:a, b name, b (a 是在 B(String name) 这个构造器中调用super() 输出的)
    注意: super() 和 this()都只能放在构造器第一行,因此这两个方法不能共存在一个构造器
    
  7. java所有类都是 Object类 的子类,Object是所有类的基类。(ctrl + H 查看类的继承关系)。

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

  9. 子类 最多只能继承一个 ** 父类(指直接继承),即java中是单继承机制**。

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

    • Person is a Music?
    • Music extends Person // 不合理
    • Cat extends Animal // 合理

本质+内存图(重要)

theory 理论、原理.

当子类继承父类,创建子类对象时,内存中到底发生了什么?提示:当子类对象创建好后,建立查找的关系

内存图:

image-20211123232211568

加载 和 查找关系:

  1. 类的加载顺序Object -> GrandPa -> Father -> Son. 先加载父类,再加载子类.

  2. 信息的查找顺序Son -> Father -> GrandPa -> Object.

  3. 按照查找关系返回信息:

    1. 首先看子类(本类)是否有该属性
    2. 如果子类(本类)有这个属性,并且可以访问则返回信息.
      • (其实本类的属性或方法是一定可以在本类内部访问的,因为即使是private修饰属性本类中也是可以调用的)
      • 但是在类的外部访问类的成员需要遵循访问权限
    3. 如果子类(本类)没有这个属性或方法,就看父类有没有这个属性(如果父类有该属性,并且可以访问,就返回信息..)
      • 如果有该属性但不可以访问(用修饰符修饰了),而程序员强行访问就会报错,这时候程序员可以通过提供的方法来访问该属性
      • 不要以为这个属性访问不了(用private等修饰了)就会继续向上找能访问的父类的相同属性这是不可以的真实情况是会直接报错。因为子类已经有了该属性(用debug查看),只是不能访问罢了,如果可以继续向上访问的话那不就乱了)。
    4. 如果父类没有就按照(3)的规则继续找上级父类直到Object...
    5. 如果Object 没有找到相对应的属性或方法,则提示属性或方法不存在。
    6. 方法的查找同上

    接口课后练习.

    匿名内部类的最佳实践.

    memory 内存

    disk 磁盘

    Detail 细节

super关键字

基本介绍

super 代表 运行类型 的 父类 的引用,用于访问父类的属性方法构造器

基本语法

  1. 访问父类的属性,但不能访问父类的private属性
    • super.属性名;
  2. 访问父类的方法,但不能访问父类的private方法.
    • super.方法名(参数列表);
  3. 访问父类的构造器(这点之前用过)
    • super(参数列表); 只能放在构造器的第一句,且只能出现一句!

细节和注意事项

  1. 调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类属性由子类初始化)

  2. 当子类中有和父类中的成员(属性和方法)重名时为了访问父类的成员必须通过super

  3. 如果没有重名使用super、this、直接访问是一样的效果

    class A {// 父类
        int n1 = 100;
        int n2 = 200;
    }
    class B extends A {// 子类
        int n1 = 888;
        int n4 = 666;
        public void hi() {
            // n1 与 this.n1 效果一样,都是访问的子类-B 的n1=888
            // super.n1; 则是访问父类-A 的n1=100
        }
        public void ok() {
            // n4 与 this.n4 效果一样,都是访问子类-B 的n4=666;
            // super.n4; 错误,父类-A 根本就没有 n4 这个属性,注意:它不会访问子类-B 的n4属性
        }
        public void cal() {
            // n2 与 this.n2 与 super.n2 三者效果相同,都是访问父类-A 的 n2属性
        }
    }
    // 结论
    // 无继承
    // 1.当方法的形参列表传入的形参名 和 类的属性名相同时: 使用this 和 直接访问 来区分
    // 2.当上述不同时,this 和 直接访问 的效果相同
    // 有继承
    // 3.当子类和父类都有相同的属性时: 则 使用super 来访问父类的属性;
    //                               使用this、直接访问 来访问子类的属性;
    // 4.当子类没有某属性,但是父类有, 则 使用super、使用this、直接访问,三者效果相同。
    // 5.当子类有属性,但父类没有,则 使用super 来访问父类的属性会出错;
    //                           使用this、直接访问 来访问子类属性
    //大总结:
    //   直接访问:就近原则,按 形参列表->本类属性->父类属性 的顺序来访问
    //   this: 先访问本类的属性,如果没有,则去父类找。本类属性->父类属性
    //   super: 直接访问父类的属性,不会访问子类的属性。->父类属性
    
  4. super 的访问不限于直接父类,如果爷爷类 和 本类中有同名的成员(属性和方法),也可以使用super 去访问爷爷类的成员如果多个基类(上级类)中都有同名的成员,使用 super 访问 遵循就近原则。Father -> GrandPa->Object, 当然也需要遵循访问权限的相关规则。

  5. super: 直接访问 运行类型 的 父类 的属性,不会访问子类(运行类型)的属性。->父类属性

super 和 this 的比较

No. 区别点 this (运行类型) super (运行类型的父类)
1 访问属性 访问本类中的属性,如果本类没有此属性则从父类中继续查找 直接从父类开始查找属性
2 调用方法 访问本类中的方法,如果本类没有此方法则从父类中继续查找 直接从父类开始查找方法
3 调用构造器 调用本类构造器,必须放在构造器的首行 调用父类构造器,必须放在子类构造器的首行
4 特殊 表示当前对象 子类中访问父类对象

方法重写/覆盖(Override)

基本介绍 前提:存在 继承 关系

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

Annotate 注释.

Annotate method with '@Override' 用 @Override 注释方法.

Generate sequence diagram 生成序列图.

Missing statement 缺少语句.

方法重写注意事项

方法重写也叫方法覆盖

  1. 子类的方法的形参列表,方法名称, 要和父类方法的形参列表,方法名称 完全一样。【形参列表,方法名称

  2. 子类方法的返回类型 和 父类方法返回类型一样,或者是父类 返回类型的子类。【返回类型

    • 比如: 父类返回类型是Object,子类返回类型是String。// 正确

    A clashes with B A与B冲突.

    incompatible 不兼容的.

  3. 子类方法 不能缩小 父类方法的访问权限public -> protected-> 默认 -> private访问权限

    assign weaker access privileges 分配较弱的访问权限.

    superclass 超类.

接口注意事项和细节.

方法重写 与 方法重载

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

declaration 声明.

recurses 递归.

infinitely 无限.

多态(polymorphic)

基本介绍

方法 或 对象具有多种状态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。

具体体现

  1. 方法的多态重写 和 重载 就体现多态。

  2. 对象的多态 (核心、重点、难点)

    ​ 重要的几句话(记住):

    1. 一个对象的编译类型运行类型 可以不一致.
    2. 编译类型在定义对象时就确定了不能改变.
    3. 运行类型可以改变 的,可以通过 getClass()查看运行类型.
    4. 编译类型看定义时 = 号 的左边运行类型= 号的 右边.
    5. 执行 运行类型 中的方法.
    Animal animal = new Dog();// animal 编译类型是Animal,运行类型Dog
    animal = new Cat();// animal 的运行类型变成了 Cat, 编译类型仍然是 Animal
    

前提、细节、向上向下转型

  1. 多态的前提是:两个对象(类)存在继承关系。

  2. 多态的向上转型

    1. 本质父类的引用指向了子类的对象.

    2. 语法父类类型 引用名 = new 子类类型();

    3. 特点

      • 编译类型看左边运行类型看右边
      • 可以调用父类中的所有成员(属性和方法)(需要遵守访问权限)
      • 不能调用子类的特有成员.
        • 因为在编译阶段能调用哪些成员,已经由 编译类型 决定了。
      • 最终运行效果看子类(运行类型)的具体体现,即调用方法时,按照从子类(运行类型)开始查找方法,然后调用,规则和前面的方法调用规则一样
  3. 多态的向下转型

    1. 语法:子类类型 引用名 = (子类类型) 父类引用;

    2. 只能强转父类的引用不能强转父类的对象

    3. 要求父类的引用 必须指向 的是 当前目标类型的对象

      //cat 的编译类型:Cat;运行类型:Cat(没有改变) 注意: 已经不是Animal了!!不是!不是!!
      // 向下转型不会改变运行类型,而是会改变编译类型(Animal->Cat)
      Cat cat = (Cat) animal;
      
    4. 当向下转型后,可以调用子类类型所有的成员 依旧可以调用父类中的所有成员( 因为继承关系 )

  4. 属性没有重写之说属性的值 **看 编译类型 **。

  5. instanceof 比较操作符。

    • 对象 instanceof XX:用于判断 对象的 运行类型 是否为 XX 类型 或 XX 类型的 类型。
    • 上面的对象不管它的编译类型是什么,父类、子类都可以 XX为类名。和编译类型无关!!!

多态应用.

匿名内部类的最佳实践.

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

Dynamic 动态.

Bind 绑定.

提醒:java已经学到这里了,我们看到代码应该要建立 编译类型 和 运行类型的概念,如果没有,那就现在建立起来!如果还能 建立 向上转型 和 向下转型的概念 那更好!

规则

  1. 调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定。

    • 加上 this 也遵循动态绑定机制; this 指向当前 运行类型 的对象, super 指向当前 运行类型 的父类的对象.
  2. 调用对象属性时,没有动态绑定机制,哪里声明,那里调用(使用)。

    class A {// 父类
        public int i = 10;
        
        public int sum() {
            return getI() + 10;// 等价于 this.getI() + 10; 均遵循动态绑定机制
        }
        public int sum1() {
            return i + 10;
        }
        public int getI() {
            return i;
        }
    }
    // 情况1 B 有sum(),sum1()
    class B extends A {// 子类
        public int i = 20;
        
        public int sum() {
            return i + 20;
        }
        public int getI() {
            return i;
        }
        public int sum1() {
            return i + 10;
        }
    }
    // main 方法中
    // a 的编译类型 A; 运行类型 B
    A a = new B();// 向上转型
    System.out.println(a.sum());// 40
    System.out.println(a.sum1());// 30
    // 分析
    // 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定。
    // 当调用对象属性时,没有动态绑定机制,哪里声明,那里调用。
    // a.sum() 1.调用方法,首先看 a 的运行类型 发现是B
    //         2.然后去子类B 找是否有sum() 
    //         3.发现有,就调用.方法体: i + 20,其中i是属性,没有动态绑定机制,所有就是本类的i = 20
    //         4.返回结果 20 + 20 = 40
    // a.sum1() 同上
    
    // 为了方法查看A类,所以复制了一份
    class A {// 父类
        public int i = 10;
        
        public int sum() {
            return getI() + 10;
        }
        public int sum1() {
            return i + 10;
        }
        public int getI() {
            return i;
        }
    }
    // 情况2 B 没有sum(),sum1()
    class B extends A {// 子类
        public int i = 20;
        
        public int getI() {
            return i;
        }
    }
    // main 方法中
    // a 的编译类型 A; 运行类型 B
    A a = new B();// 向上转型
    System.out.println(a.sum());// 30
    System.out.println(a.sum1());// 20
    // 分析
    // 当调用对象方法的时候,该方法会和该对象的内存地址/运行类型绑定。
    // 当调用对象属性时,没有动态绑定机制,哪里声明,那里调用。
    // a.sum() 1.调用方法,先看a 的运行类型,发现是 B
    //         2.然后去B类中找sum() 方法,没有找到
    //         3.根据继承关系,再去B 的父类A 中找sum()方法,找到了
    //         4.调用父类A的sum() 方法体: getI() + 10;
    //          敲黑板了啊!!重点来了
    //         5.getI()是方法,根据方法的动态绑定机制,看运行类型B 中是否有getI()方法,发现有,调用
    //          这时调用的方法getI() 是B 类的getI()方法
    //         6.getI() 方法体: i, i 是属性,没有动态绑定机制,首先查看当前类的属性是否有i,
    //         7.发现有,那就调用,返回i = 20,即getI() 为20
    //         8.最终结果: getI() + 10 = 20 + 10 = 30
    //================================================================================
    // a.sum1() 1.调用方法,先看a 的运行类型,发现是B
    //          2.然后去B类中找sum1() 方法,没有找到
    //          3.根据继承关系,再去它的父类A 中找sum1() 方法,找到了
    //          4.调用父类A 的sum1() 方法,方法体:i + 10,其中 i 是属性,没有动态绑定机制,首先
    //          查看本类A 是否有属性 i,发现有,则调用 i = 10
    //          5.最终结果:i + 10 = 10 + 10 = 20
    

抽象类最佳实践-模板设计模式.

匿名内部类的最佳实践.

多态应用

1.多态数组

基本介绍数组定义类型 为 父类类型里面保存的实际元素 为 子类类型

--》 复习 java动态绑定机制 和 向下向上转型

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

细节和注意事项(前提、向上向下转型).

2.多态参数

基本介绍方法定义形参类型父类类型实参类型允许为子类类型

--》 复习 java动态绑定机制 和 向下向上转型

Object类 详解

equals方法

== 和 equals 的对比
  1. == 是一个比较运算符.

    1. == :可以判断基本类型可以判断引用类型
    2. == :如果判断基本数据类型,判断的是值是否相等。eg. int i = 10; double d = 10.0;// i==d true.(这里会进行自动类型转换,int --> double )
    3. == :如果判断引用类型,判断的是地址是否相等,即判定是不是同一个对象
  2. equals 方法.

    1. equals:是 object类中的方法,只能判断引用类型
    2. 默认判断的是地址是否相等子类中往往重写该方法用于判断内容是否相等。比如Integer,String
    char c = 12;
    System.out.println(c == 12);// true
    
  3. 名称 概念 用于基本数据类型 用于引用类型
    == 比较运算符 可以,判断是否相等 可以,判断两个对象是否相等(判断是否是同一对象)
    equals Object类的方法,java类都可以使用equals 不可以 可以,默认是判断两个对象是否相等,但是子类往往重写该方法比较对象的属性是否相等,比如(String, Integer)
  4. equals方法 重写

class Doctor {
    private String name;
    private char gender;
    
    public Doctor(String name, char gender) {
        this.name = name;
        this.gender = gender;
    }
    // 重写equals方法
    @Override
    public boolean equals(Object obj) {
        // 判断两个比较对象是否相同
        if (this == obj) {
            return true;
        }
        // 判断 obj 是否是 Doctor类型或其子类
        // 过关斩将 校验方式
        if (!(obj instanceof Doctor)) {
            return false;
        }
        // 向下转型,因为 obj 的运行类型是 Doctor 或其子类
        Doctor doctor = (Doctor)obj;
        return this.name.equals(doctor.name) && this.gender == doctor.gender;
    }
}

hashCode 方法

  1. 提高具有哈希结构容器的效率
  2. 两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
  3. 两个引用,如果指向的是不同对象,则哈希值是不一样
  4. 哈希值主要根据地址号来的!,不能完全将哈希值等价于地址
  5. obj.hashCode().
  6. 后面的集合中 hashCode() 如果需要的话,也会重写。

toString 方法

基本介绍

默认返回全类名 + @ + 哈希值的十六进制

  1. 全类名:包名 + 类名

  2. 哈希值是通过hashCode() 得到的十六进制整数.

  3. 子类往往重写toString方法,用于返回对象的属性信息

  4. 重写toString方法,打印对象或拼接对象时,都会自动调用该对象的toString形式。

  5. 直接输出一个对象时,toString方法会被默认的调用

    System.out.println( father )默认调用 father.toString().

    // Object的toString() 源码
    // 1.getClass().getName() 类的全类名(包名 + 类名)
    // 2.Integer.toHexString(hashCode()) 将对象的hashCode值转成16进制的字符串
    public String toString() {
        return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    

finalize 方法

基本介绍

  1. 对象被回收时系统自动调用对象的 finalize 方法子类可以重写该方法,做一些释放资源的操作。
  2. 什么时候被回收当某个对象没有任何引用时,则 jvm认为这个对象是一个垃圾对象就会使用垃圾回收机制来销毁(回收)该对象在销毁(回收)该对象前会先调用finalize方法
  3. 垃圾回收机制的调用,是由系统来决定 (即有自己的GC算法),也可以通过 System.gc() 主动触发垃圾回收机制
  4. 提示:我们在实际开发中几乎不会运用finalize,所以更多就是为了应付面试

断点调试(debug)

重要提示和技巧

  • 断点调试 过程中,是运行状态,是以对象的 运行类型来执行的。
  • 光标 放在某个变量上可以看到最新的数据
  • 断点可以在debug过程中动态的下断点

断点调试的快捷键

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

示意图

image-20211130112510659

项目-零钱通

loop 循环.

menu 菜单.

balance 余额.

date 日期.

format 格式.

note 说明.

Mismatch 不匹配.

choice 选择.

complete 完成.

without 没有.

income 收入.

  1. 菜单设计使用 do while 循环,至少显示一次.
  2. 一块代码完成一个小功能
  3. 字符串 比较相等用: equals()
  4. 条件校验找不正确的条件,给出提示,然后结束。(过关斩将 校验方式)

时间格式

Date date = null; // date 是 java.util.Date 类型,表示日期
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");// 可以用于日期格式化
// 使用
date = new Date();// 获取当前日期
sdf.format(date); // 使用日期格式化

课后作业词汇

introduce 介绍.

professor 教授.

grade 级别.

employee 员工.

daySal 单日工资.

workDays 工作天数.

manager 经理.

bonus 奖金.

worker 普通员工、工人.

peasant 农民.

scientist 科学家.

waiter 服务员.

BankAccount 银行账户.

deposit 存款.

withdrawal 取款.

rate 利率.

label 标签.

房屋出租系统

1.项目设计-程序框架图

(分层模式==>当软件比较复杂,需要模式管理)

image-20211204161646793 image-20211204224118100

2.准备工具类Utility,提高开发效率.

  • 了解Utility类的使用
  • 测试Utility类

注意:1.这里直接使用 类.方法(); => 因为当一个方法是 static 时,就是一个静态方法.

​ 2.静态方法可以直接通过类名调用

3.项目功能实现-完成House类

id 编号.

rent 租金.

state 状态.

4.项目功能实现-显示主菜单和完成退出软件功能

mainMenu 主菜单.

化繁为简(一个一个功能逐步实现)

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

  1. 功能说明:
    • 用户打开软件,可以看到主菜单,可以退出软件
  2. 思路分析:
    • 在HouseView.java 中,编写一个方法 mainMenu, 显示菜单
  3. 代码实现
// 退出程序的代码实现
boolean loop = true;// 先设定运行程序loop为true
while (loop) {
    if (exit) {// 当满足exit条件时,就将loop改为false,即退出系统(循环)
        loop = false;
    }
}

5.项目功能实现-完成显示房屋列表的功能

各司其职

  1. 功能说明
  2. 思路分析
    • 需要编写HouseView.java 和 HouseService.java
  3. 代码实现

6. 项目功能实现-完成添加房屋信息的功能

  1. 功能说明
  2. 思路分析
  3. 代码实现

7. 项目功能实现-完成删除房屋信息的功能

  1. 功能说明.
  2. 思路分析
  3. 代码实现
// 根据id找下标 注意: id 与 下标 不是一回事
int index = -1;
for (int i = 0; i < housesNum; i++) {
    if (id = houses[i].getId()) {
        index = i;// 用index来记录下标i的值
    }
}
// 删除房屋信息
for (int i = index; i < housesNum - 1; i++) {// 注意: 这里时housesNum - 1,别忘了减一
    houses[i] = houses[i + 1];// 从index开始向前移动
}
houses[--housesNum] = null;

8.项目功能实现-完善退出确认功能

  1. 功能说明.
  2. 思路分析
  3. 代码实现

9. 项目功能实现-完成根据id查找房屋信息的功能

findById 通过 id 查找.

技巧:可以用 ideastructure快速定位方法

  1. 功能说明.
  2. 思路分析
  3. 代码实现

10.项目功能实现-完成修改房屋信息的功能

  1. 功能说明.
  2. 思路分析
  3. 代码实现

类变量 和 类方法

static关键字

static作用

  • 只能修饰在类里定义的成员部分.

  • eg.【成员变量、方法、内部类(枚举与接口)、代码块(初始化块)

  • 不能修饰外部类,不能修饰局部变量,局部内部类

类变量(静态变量)

基本介绍

类变量,也称为静态变量:用static修饰,最大的特点是:该变量可以被类的所有的对象实例共享

内存布局

jdk1.7、1.8之前 -> 类变量(静态变量) 放在方法区的静态域中

image-20211205182703152

jdk1.7、1.8之后 -> 类变量(静态变量) 放在堆中

image-20211205190636888

记住一点static 变量是对象共享

不管static变量在哪里:(共识)

  1. static 变量 同一个类 所有对象共享
  2. static(类)变量 , 在类加载的时候就生成了
    • 类变量随着类的加载而创建的,所有即使没有创建对象实例也可以访问.

类变量定义访问

什么是类变量

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

如何定义类变量

定义语法

  1. 访问修饰符 static 数据类型 变量名; [推荐]
  2. static 访问修饰符 数据类型 变量名;

如何访问类变量

  1. 类名.类变量名 [推荐]
  2. 对象名.类变量名

注意:【静态变量的访问修饰符访问权限和范围普通属性是一样的

注意事项和细节

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

    • 当我们需要 让某个类的所有对象都共享一个变量时就可以考虑使用类变量(静态变量):比如:定义学生类,统计所有学生共交多少钱。Student(name, static fee)

      fee 费用、学费.

  2. 类变量 与 实例变量(普通变量) 区别

    • 类变量是该类的所有对象共享的,而实例变量是每个对象独享的.
  3. 加上 static 称为类变量或静态变量否则称为实例变量/普通变量/非静态变量.

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

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

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

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

类方法(静态方法)

基本介绍

形式

  1. 访问修饰符 static 数据返回类型 方法名() { } 【推荐
  2. static 访问修饰符 数据返回类型 方法名()

类方法的调用

使用形式:

  1. 类名.类方法名推荐
  2. 对象.类方法名

前提是:满足访问修饰符访问权限和范围.

类方法经典的使用场景【工具】

方法不涉及任何和 对象 相关的成员,则可以将方法设计成静态方法提高开发效率

比如:工具类中的方法utils

Math类、Arrays类、Collection 集合类

小结:在程序员实际开发中,往往会将一些通用的方法,设计成静态方法,这样我们不需要创建对象就可以使用了,比如打印一维数组,冒泡排序,完成某个计算任务。

注意事项

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

小结静态方法,只能访问静态的成员; 非静态的方法,可以访问静态成员和非静态成员。【必须遵守访问权限

main方法

理解main方法语法

深入理解main方法

解释 main 方法的形式public static void main(String[] args) { }.

  1. main 方法被虚拟机调用的.
  2. java虚拟机需要调用类的 main() 方法, 所以该方法的访问权限必须是public.
  3. java虚拟机在执行 main() 方法时不必创建对象,所以该方法必须是static.
  4. 该方法接收String类型数组参数,该数组中保存执行java命令时传递给所运行的类的参数。
  5. 格式:java 执行的程序 参数1 参数2 参数3.

特别提示:

  1. main() 方法中,我们可以 直接调用 main 方法所在类的静态方法或属性.
  2. 但是,不能直接访问 该类中 的非静态成员必须创建该类的一个实例对象后才能通过这个对象去访问类中的非静态成员

main动态传值 (命令行参数)

Configuration 配置.

Program arguments 程序参数(命令行参数).

如图所示:

image-20211206102303990

代码块 (CodeBlock)

基本介绍

代码块 又称为 初始化块,属于类中的成员[即 是类的一部分],类似于方法,将逻辑语句封装在方法体中,通过 {} 包围起来.

但和方法不同,没有方法名,没有返回,没有参数,只要方法体,而且不用 通过对象或类显式调用,而是加载类时,或创建对象时隐式调用

注意:方法 需要 通过对象或类 显式调用. 方法不调用就不会起作用.

基本语法

[修饰符] {
    代码
};

注意

  1. 修饰符 可选,要写的话,也只能写 static.
  2. 代码块分为两类,使用 static 修饰的叫 静态代码块没有 static 修饰的,叫普通代码块/非静态代码块.
  3. 逻辑语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
  4. ; 号可以写上,也可以省略。【可选

代码块的好处

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

注意事项和细节

  1. static 代码块也叫静态代码块,作用就是对类进行初始化,而且它随着 类的加载 而执行,并且 只会执行一次(因为类只会加载一次)。如果是普通代码块每创建一个对象,就会执行一次

  2. 类什么时候被加载 (3种情况)【重要背

    1. 创建对象实例时 (new)
    2. 创建子类对象实例,父类也会被加载.(而且,父类先被加载,子类后被加载)
    3. 使用类的静态成员时(静态属性,静态方法)
      • 使用子类的静态成员,父类也会被加载.
      • 子类加载之前,父类先加载. 父亲先有,然后才有儿子.
  3. 普通的代码块在创建对象实例时会被隐式的调用

    被创建一次,就会调用一次

    如果只是使用类的静态成员时,普通代码块并不会执行

    可以简单的理解普通代码块是构造器的补充.

  4. 类加载 和 创建对象实例 不是一回事

    小结

    1. static代码块是类加载时,执行,只会执行一次
    2. 普通代码块 是在创建对象时调用的,创建一次,调用一次
    3. 类加载的3种情况,需要记住.
  5. 创建一个对象时,在 一个类 调用顺序是:(重点,难点理解记忆)

    1. 调用静态代码块 和 静态属性初始化(注意:静态代码块 和 静态属性初始化调用的优先级一样,如果有多个静态代码块 和 多个静态变量初始化,则们定义的顺序调用)
    2. 调用普通代码块 和 普通属性的初始化(注意:普通代码块 和 普通属性初始化调用的优先级一样,如果有多个普通代码块 和 多个普通属性初始化,则按定义顺序调用)
    3. 调用构造方法
  6. 构造器(构造方法)最前面其实隐含了(1) super() 和 (2)调用普通代码块和普通属性初始化静态相关的代码块、属性初始化,在类加载时,就执行完毕,因此是优先于 构造器和普通代码块执行的。

    class A {
        public A() {// 构造器
            // 这里有隐藏的执行要求
            // (1)super();// 在继承的时候讲过.这里注意 没有 默认调用this()!!!
            // (2)调用普通代码块和普通属性初始化
            System.out.println("ok");
        }
    }
    
  7. 创建一个子类对象时(继承关系),他们的静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法(构造器)的调用顺序如下:【面试题】

    1. 父类的静态代码块和静态属性(优先级一样,按定义顺序执行).(首先为类变量分配空间)
    2. 子类的静态代码块和静态属性(优先级一样,按定义顺序执行).(首先为类变量分配空间)
    3. 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行).(首先为实例变量分配空间)
    4. 父类的构造方法(构造器).(首先为实例变量分配空间)
    5. 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行).(首先为实例变量分配空间)
    6. 子类的构造方法(构造器).(首先为实例变量分配空间)
  8. 静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员

实例代码一

class A02 {// 父类
    private static int n1 = getVal01();// 静态属性
    static {// 静态代码块
        System.out.println("2.A02的一个静态代码块..");
    }
    {// 普通代码块
        System.out.println("5.A02的一个普通代码块..");
    }
    public int n3 = getVal02();// 普通属性
    
    public static int getVal01() {
        System.out.println("1.getVal01");
        return 10;
    }
    public static int getVal02() {
        System.out.println("6.getVal02");
        return 10;
    }
    public A02() {// 构造器
        //隐藏了
        //(1)super()
        //(2)普通代码块和普通属性初始化
        System.out.println("7.A02构造器");
    }
}
class B02 extends A02 {// 子类
    private static int n3 = getVal03();// 静态属性
    static {// 静态代码块
        System.out.println("4.B02的一个静态代码块..");
    }
    public int n4 = getVal04();// 普通属性
    {// 普通代码块
        System.out.println("9.B02的一个普通代码块..");
    }

    public static int getVal03() {
        System.out.println("3.getVal03");
        return 10;
    }
    public static int getVal04() {
        System.out.println("8.getVal04");
        return 10;
    }
    public B02() {// 构造器
        // 隐藏了
        //(1)super()
        //(2)普通代码块和普通属性初始化
        System.out.println("10.B02构造器");
    }
}
// main方法
public static void main(String[] args) {
    // 说明
    //(1) 进行类的加载
    //1.1 先加载 父类A02 1.2 再加载 子类B02
    //(2) 创建对象
    //2.1 从子类构造器开始
    new B02();
}

示例代码二


单例设计模式

instance 实例.

single 单.

什么是设计模式

  1. 静态方法和属性的经典使用
  2. 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和摸索。

什么是单例模式

单例(单个的实例)

  1. 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
  2. 单例模式有两种方式:(1) 饿汉式 (2)懒汉式

单例设计模式-饿汉式

步骤【饿汉式】:

  1. 构造器私有化 =》 防止在外部直接 new.
  2. 在类的内部直接创建对象 (该对象是static).
  3. 向外暴露(提供)一个静态的公共方法,返回对象实例。getInstance

注意:(1) 该对象是重量级对象 (2) 饿汉式可能创建了对象,但是没有使用,造成资源浪费

class GirlFriend {
    // 饿汉式
    // (1) 构造器私有化
    // (2) 在类的内部直接创建对象
    // (3) 向外提供一个公共的静态方法,用来返回对象实例
    private String name;// 属性
    private static GirlFriend gf = new GirlFriend("小白");// 对象实例

    private GirlFriend(String name) {// 构造器私有化
        this.name = name;
    }
    public static GirlFriend getInstance() {// getInstance()方法
        return gf;
    }
}

单例设计模式-懒汉式

步骤【懒汉式】:

  1. 构造器私有化 =》 同上
  2. 定义一个static静态属性对象.
  3. 提供一个public的static方法,可以返回一个对象.
  4. 懒汉式,只有当用户使用getInstace方法时,才返回对象。后面再次调用时,会返回上次创建的对象,从而保证了单例。
class BoyFriend {
    // 懒汉式
    // (1) 构造器私有化
    // (2) 定义一个static静态属性对象
    // (3) 提供一个public的static方法,返回一个对象
    private String name;// 属性
    private static BoyFriend bf;// 定义对象引用
    private BoyFriend(String name) {// 构造器私有化
        this.name = name;
    }
    public static BoyFriend getInstance() {// getInstance()方法
        if (bf == null) {// 如果对象为null,则实例化一个对象
            bf = new BoyFriend("小明");
        }
        return bf;
    }
}

饿汉式 VS 懒汉式

  1. 二者最主要的区别在于创建对象的时机不同:饿汉式在类加载就创建了对象实例,而懒汉式在使用时才创建。
  2. 饿汉式不存在线程安全问题懒汉式存在线程安全问题。(后面会完善)
  3. 饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
  4. 在我们javaSE标准类中,java.lang.Runtime就是经典的单例模式。
  5. 小结:
    1. 单例模式的两种实现方式(1) 饿汉式 (2)懒汉式。
    2. 饿汉式问题:在类加载的时候就创建,可能存在资源浪费问题。
    3. 懒汉式问题:线程安全问题,后面学了线程再完善。
    4. 需要独立的写出单例模式。

final 关键字

基本介绍

final 中文意思:最后的,最终的.

final 可以修饰属性方法局部变量. 只能用不能改

在某些情况下,程序员可能有以下需求,就会使用到 final:

  1. 不希望类被继承时,可以final 修饰
  2. 不希望父类的某个方法被子类覆盖/重写(override)时,可以final 关键字修饰
  3. 不希望类的某个属性的值被修改(常量),可以final 修饰

TAX_RATE 税率.

  1. 不希望某个局部变量被修改(局部常量),可以使final 修饰

assign 分配.

value 值.

variable 变量.

注意事项和细节

  1. final 修饰的属性又叫常量(不变的量),一般用 XX_XX_XX 来命名.

  2. final 修饰的属性在定义时,必须赋初始值并且以后不能再修改,赋值可以在如下位置之一【选择一个位置赋初值即可,且只能有一处赋初值,如果有多处赋值的话 那不就又成了修改值了】:

    1. 定义时:如 public final double TAX_RATE = 0.08;
    2. 在构造器中
    3. 在普通代码块中
  3. 如果 final 修饰的属性是静态的,则初始化的位置只能是:

    1. 定义时.
    2. 在静态代码块,不能在构造器中赋值.
  4. final 类不能继承,但是可以实例化对象

  5. 如果类不是final类,但是含有final方法,则该方法虽然不能重写但是可以被继承,供子类使用

  6. 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法。

  7. final 不能修饰构造方法(即构造器).

    modifier 修饰符.

  8. final 和 static 往往搭配使用效率更高,而且不会导致类加载. 底层编译器做了优化处理。

    class AAA {
        public static final int n1 = 120;
        static {
            System.out.println("AAA 静态代码块");
        }
    }
    
  9. 包装类(Integer, Double, Float, Boolean等都是final),String也是final类

抽象类 (abstract)

父类方法的不确定性

父类方法不确定性问题:

  • 考虑将该方法设计为抽象(abstract)方法.
  • 所谓抽象方法就是 没有实现的方法.
  • 所谓没有实现的方法就是指,没有方法体.
  • 当一个类中存在抽象方法时,需要将该类声明为abstract类.
  • 一般来说,抽象类会被继承,由其子类来实现抽象方法.

小结:当父类的某些方法,需要声明,但是又不明确如何实现的时候可以将其声明为抽象方法,那么这个类就是抽象类

declare 声明.

父类的一些方法不能确定时,可以用 abstract 关键字来修饰该方法,这个方法就是抽象方法,用 abstract 来修饰该类就是抽象类

抽象类介绍

  1. 用abstract 关键字来修饰一个类时,这个类就叫抽象类

    访问修饰符 abstract 类名 { }.

  2. 用abstract 关键字来修饰一个方法时,这个方法就是抽象方法

    访问修饰符 abstract 返回类型 方法名(形参列表); //没有方法体.

  3. 抽象类的价值更多作用是在于设计,是设计者设计好后,让子类继承并实现。

  4. 抽象类,是考官比较爱问的知识点,在框架和设计模式使用较多

注意事项和细节

  1. 抽象类不能被实例化

  2. 抽象类不一定要包含abstract方法。也就是说,抽象类可以没有abstract方法,还可以有实现的方法。

  3. 一旦类包含了abstract方法,则这个类必须声明为abstract类.

  4. abstract 只能修饰类和方法,不能修饰属性和其它的。

  5. 抽象类可以有任意成员抽象类本质还是类】,比如:非抽象方法、构造器、静态属性等等

  6. 抽象方法不能有主体,即不能实现。

  7. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法除非它自己也声明为abstract类

    所谓实现方法,就是有方法体

  8. 抽象方法不能使用 privatefinalstatic 来修饰,因为这些关键字都是和重写相违背的.

抽象类最佳实践-模板设计模式

需求

  1. 有多个类,完成不同的任务job
  2. 要求统计得到各自完成任务的时间
  3. 请编程实现

Template 模板.

calculate 计算.

分析

设计一个抽象类(Template),能完成如下功能:

  1. 编写方法calculateTime(),可以计算某段代码的耗时时间.
  2. 编写抽象方法job().
  3. 编写一个子类Sub,继承抽象类Template,并实现job方法。
  4. 编写一个测试类TestTemplate,看看是否好用。

代码实现

current 现在的,当前的.

public abstract class Template {// 抽象类
    public abstract void job();// 抽象方法
    public void calculate() {// 统计耗时多久,是确定的方法
        // 统计当前时间距离 1970-1-1 0:0:0 的时间差,单位ms(毫秒)
        long start = System.currentTimeMillis();
        job();// 动态绑定机制!!! 
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start));
    }
}

java动态绑定机制复习.Ctrl + 鼠标左键=》跳转.

接口 (interface)

基本介绍

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

implement 实现.

语法

interface 接口名 {
    // 属性
    // 方法(1.抽象方法2.默认实现方法3.静态方法)
}
class 类名 implements 接口 {
    // 自己属性
    // 自己方法
    // 必须实现的接口的抽象方法
}

小结

  1. jdk7.0 前,接口里的所有方法都没有方法体,即都是抽象方法
  2. jdk8.0 后接口可以有静态方法(static修饰)默认方法(这里必须显式定义,default修饰),也就是说接口中可以有方法的具体实现

接口应用场景

统一规范

需要以后积累...

接口注意事项和细节

自定义泛型接口.

  1. 接口不能被实例化.

  2. 接口中所有的方法是 public 方法,接口中抽象方法,可以不用 abstract 修饰(默认有 abstract.

    redundant 多余的.

    方法重写注意事项第3条.

  3. 一个普通类实现接口,就必须将该接口的所有抽象方法都实现.(可以使用 alt + enter 快捷键)

  4. 抽象类实现接口,可以不用实现接口的方法.

  5. 一个类同时可以实现多个接口,用 逗号 分开.

  6. 接口中的属性,只能是final的,而且是 public static final 修饰符

    比如:int a = 1; 实际上是 public static final int a = 1; (必须初始化)

  7. 接口中属性的访问形式:接口名.属性名

  8. 接口不能继承其它的类,但是可以继承多个别的接口, 接口不能实现接口

    interface A extends B,C

  9. 接口的修饰符,只能是 public 和 默认,这点和类的修饰符是一样的

    类与对象.

接口课后练习

interface A {
     int n1 = 23;// 等价于 public static final int n1 = 23;
}
class B implements A {
}
// main方法
public static void main(String[] args) {
    B b = new B();
    System.out.println(b.n1);// 23
    System.out.println(B.n1);// 23,这个注意一下
    System.out.println(A.n1);// 23
}

[加载 和 查找关系](#加载 和 查找关系).

实现接口 VS 继承类

当子类继承了父类,就自动的拥有父类的功能;如果子类需要扩展功能,可以通过实现接口的方法扩展;

可以理解为:实现接口 是 对java 单继承机制的一种补充

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

接口的多态特性

  1. 多态参数 (InterfacePolyParameter)

    前面的Usb接口案例,UsbInterface usb,既可以接收手机对象,又可以接收相机对象,就体现了接口多态

    接口引用 可以指向 实现了接口类的实例对象.

    interface A {}
    class C implements A {}
    class D implements A {}
    // 方法内
    A a1 = new C();
    A a2 = new D();
    
  2. 多态数组(InterfacePolyArr) ==》接口类型数组

    interface A {}
    class B implements A {}
    class C implements A {}
    // 方法内
    A[] a = new A[2];
    a[0] = new B();
    a[1] = new C();
    
  3. 接口存在多态传递现象(InterfacePolyPass)

    interface A {}
    // 实际上就相当于 C 类 也实现了 A 接口
    interface B extends A {}
    class C implements B {}
    

接口课堂练习

ambiguous 模棱两可的.

interface A {
    int x = 0;// 想到 等价于 public static final int x = 0;
}
class B {
    int x = 1;// 普通属性
}
class C extends B implements A {
    public void pX() {
        //System.out.println(x);// 有问题,不明确x是父类的,还是接口的
        // 可以明确的指定x
        // 访问接口的 x 就使用 A.x
        // 访问父类的 x 就使用 super.x
        System.out.println(A.x + " " + super.x);// 0 1
    }
    public static void main(String[] args) {
        new C().pX();
    }
}

内部类

基本介绍

一个类的内部又完整的嵌套了另一个类结构嵌套的类称为内部类(inner class)嵌套其他类的类称为外部类(outer class)。是我们类的第五大成员【思考:类的五大成员是哪些?[属性、方法、构造器、代码块、内部类]】,内部类最大的特点是可以直接访问私有属性并且可以体现类与类之间的包含关系,注意:内部类是学习的难度,同时也是重点,后面看底层源码时,有大量的内部类。

基本语法

class Outer {// 外部类
    class Inner {// 内部类
    }
}
class Other {// 外部其他类
}

内部类的分类(4种)

  • 定义在外部类局部位置上(比如方法内/代码块内):
    1. 局部内部类(有类名)
    2. 匿名内部类(没有类名,重点)
  • 定义在外部类的成员位置上:
    1. 成员内部类(没有static修饰)
    2. 静态内部类(使用static修饰)

匿名内部类的最佳实践.

局部内部类的使用

说明:(LocalInnerClass)

  1. 局部内部类是定义在外部类的局部位置,比如方法中/代码块中并且有类名.

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

  3. 不能添加访问修饰符,因为它的地位就是一个局部变量。局部变量是不能使用修饰符的。但是可以使用final修饰,因为局部变量也可以使用final.

  4. 作用域:仅仅在定义它的方法或代码块中.

  5. 局部内部类 -- 访问 ----> 外部类的成员.

    访问方式:直接访问(包括私有的)】

  6. 外部类 --- 访问 ----> 局部内部类的成员.(注意:必须在作用域内)

    访问方式创建对象,再访问(包括私有成员)】

  7. 外部其他类--- 不能访问 -----> 局部内部类(因为 局部内部类地位是一个局部变量

  8. 如果外部类和局部内部类成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用:

    外部类.this.成员名)去访问

    需要解释一下原因:

    class Outer {// 外部类
        private int n1 = 10;
        public void m1() {
            class Inner {// 局部内部类
                // 外部类与局部内部类成员重名
                private int n1 = 99;
                public void m2() {
                    // 默认遵循就近原则
                    System.out.println(n1);// 99
                    // this 本质就是内部类的对象
                    // 即哪个对象调用了m2(), this 就是那个对象-->inner
                    System.out.println(this.n1);// 99
                    // Inner.this 本质就是内部类的对象
                    // 即哪个对象调用了m2(), Inner.this 就是那个对象-->inner
                    System.out.println(Inner.this.n1); // 99
                    // Outer.this 本质就是外部类的对象,
                    // 即哪个对象调用了m1(), Outer.this 就是那个对象-->outer
                    System.out.println(Outer.this.n1);// 10
                }// m2()方法结束
            }// Inner类结束
            Inner inner = new Inner();
            inner.m2();
        }// m1()方法结束
    }// Outer类结束
    // main方法
    Outer outer = new Outer();
    outer.m1();
    

记住

  1. 局部内部类定义在方法中/代码块
  2. 作用域在方法体或者代码块中
  3. 本质仍然是一个类.

匿名内部类的使用(重要!!!!)

基本介绍

Anonymous 匿名的.

实质:(1) 本质还是一个类. (2)内部类. (3)该类没有名字. (4)同时还是一个对象.

说明匿名内部类是定义在外部类的局部位置,比如方法中/代码块中并且没有类名

匿名内部类的基本语法

new 类或接口(参数列表) {
    类体
};
class Outer01 {
    public void method() {
        // 一、基于接口类的匿名内部类
        // 解读:
        // 1.需求:想使用IA接口,并创建对象
        // 2.传统方式,是写一个类,实现该接口,并创建对象
        // 3.需求是 Tiger/Dog 类只是使用一次,后面再不使用
        // 4.可以使用匿名内部类来简化开发
        // 5.tiger的编译类型?IA
        // 6.tiger的运行类型?就是匿名内部类 XXXX => Outer01$1  
        // 运行类型查看用 tiger.getClass();
        /*
        	我们看底层 会分配 类名XXXX=>Outer01$1
        	class XXXX implements IA {
        		@Override
                public void cry() {
                    System.out.println("老虎叫..");
                }
        	}
        */
        // 7.jdk底层在创建匿名内部类 Outer01$1,立即马上就创建了 Outer01$1实例,
        //   并且把地址返回给 tiger
        // 8.匿名内部类使用一次,就不能再使用(在底层生成完,返回一个实例,这个匿名内部类就没有了)
        // 9.而 tiger对象可以多次使用
        // 10.匿名内部类不是说这个类没有名字,而是类用完了就没有了
        IA tiger = new IA() {
            @Override
            public void cry() {
                System.out.println("老虎叫..");
            }
        };
        System.out.println("tiger的运行类型=" + tiger.getClass());
        tiger.cry();
        tiger.cry();
        tiger.cry();
        
        // 二、基于类的匿名内部类
        // 1.father编译类型 Father
        // 2.father运行类型 Father(错错错,错的很彻底) 应该是 Outer01$2
        // 3.底层会创建匿名内部类
        /*
			class Outer01$2 extends Father {
				@Override
                public void test() {
                    System.out.println("匿名内部类重写了test方法");
                }
			}
        */
        // 4.同时也直接返回了 匿名内部类 Outer01$2的对象
        // 5.注意:("jack") 参数列表会传递给构造器
        Father father = new Father("jack") {
            @Override
            public void test() {
                System.out.println("匿名内部类重写了test方法");
            }
        };
        System.out.println("father对象的运行类型=" + father.getClass());
        father.test();
        
        // 三、基于抽象类的匿名内部类
        Animal animal = new Animal() {
            @Override
            public void eat() {
                System.out.println("匿名内部类重写了eat方法");
            }
        }
    }
}
interface IA {// 接口
    void cry();
}
class Father {// 类
    public Father(String name) {//构造器
        System.out.println("接收到 name=" + name);
    }
    public void test() {// 方法
        
    }
}
abstract class Animal {// 抽象类
    abstract void eat();
}

匿名内部类的细节

  1. 匿名内部类的语法比较奇特,请大家注意,因为匿名内部类 既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征,对前面代码分析可以看出这个特点,因此可以调用匿名内部类方法。

    • class A {// 类
          public void ok() {
              System.out.println("ok~");
          }
      }
      // 基于类的匿名内部类
      // 方法一、创建对象,在调用方法(体现了匿名内部类是一个类)
      A a = new A() {
          @Override
          public void ok() {
              System.out.println("1.匿名内部类重写了ok方法");
          }
      }
      a.ok();// 动态绑定
      // 方法二、直接调用方法(体现了匿名内部类是一个对象)
      new A() {
          @Override
          public void ok() {
              System.out.println("2.匿名内部类重写了ok方法");
          }
      }.ok();
      
  2. 可以直接访问外部类的所有成员包括私有的

  3. 不能添加访问修饰符,因为它的地位就是一个局部变量。

  4. 作用域:仅仅在定义它的方法或代码块中

  5. 匿名内部类 --- 访问 ----> 外部类成员【访问方式:直接访问

  6. 外部其他类 --- 不能访问 ---> 匿名内部类(因为 匿名内部类的地位是一个局部变量)

  7. 如果外部类和匿名内部类的成员重名时,匿名内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问。

匿名内部类的最佳实践

当做实参直接传递,简洁高效

// 接口
interface IA {
    void cry();
}
// 形参是接口类型
class A {
    public void show(IA ia) {
        ia.cry();
    }
}
// main方法
A a = new A();
// 当做实参直接传递,简洁高效
a.show(new IA() {
    @Override
    public void cry() {
        System.out.println("叫声有点大哈...");
    }
});
// 案例一:手机铃声
/*
   1.有一个铃声接口Bell,里面有个ring方法。
   2.有一个手机类CellPhone,具有闹钟功能alarmClock,参数是Bell类型
   3.测试手机类的闹钟功能,通过匿名内部类(对象)作为参数,打印:懒猪起床了
   4.再传入另一个匿名内部类(对象),打印:小伙伴上课了
*/
public class HomeWork0401 {
    public static void main(String[] args) {
        CellPhone2 c1 = new CellPhone2();
        // 解读
        // 1.参数传递的是实现了 Bell接口的匿名内部类 HomeWork0401$1
        // 2.匿名内部类重写了 ring()方法
        // 3.匿名内部类最终是传给了 public void alarmClock(Bell bell)中的 bell
        //   相当于 Bell bell = new Bell() {
        //                      @Override
        //                      public void ring() {
        //                          System.out.println("懒猪起床了");
        //                      }
        //                  }
        //
        
        
        c1.alarmClock(new Bell() {
            @Override
            public void ring() {
                System.out.println("懒猪起床了");
            }
        });
        CellPhone2 c2 = new CellPhone2();
        c2.alarmClock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上课了");
            }
        });
    }
}
interface Bell {// 接口
    void ring();// 方法
}
class CellPhone2 {// 类
    public void alarmClock(Bell bell) {// 形参是Bell接口类型
        System.out.println(bell.getClass());// 输出bell的运行类型
        bell.ring();// 动态绑定,找运行类型
    }
}

// 案例二:计算功能
// 1.计算器接口ICalculate 具有work方法,功能是运算,有一个手机类CellPhone,定义方法testWork测试计算功能, 调用计算接口的work方法
// 2.要求调用CellPhone对象 的testWork方法,使用 匿名内部类
// 考察对匿名内部类的使用
public class HomeWork04 {
    public static void main(String[] args) {
        // 解读
        // 1.匿名内部类
        /*
        	new ICalculate() {
                @Override
                public double work(double n1, double n2) {
                    return n1+n2;
                }
            },同时也是一个对象
            它的编译类型 ICalculate,它的运行类型就是 匿名内部类
        */
        CellPhone cellPhone = new CellPhone();
        cellPhone.testWork(new ICalculate() {
            @Override
            public double work(double n1, double n2) {
                return n1+n2;
            }
        }, 10, 2);// 12
        cellPhone.testWork(new ICalculate() {
            @Override
            public double work(double n1, double n2) {
                return n1*n2;
            }
        }, 10, 2);// 20
    }
}
interface ICalculate {// 接口
    // work方法 是完成计算,但是题目没有具体要求,所有自己实现
    // 至于该方法完成怎样的计算,我们交给匿名内部类完成
    double work(double n1, double n2);
}
class CellPhone {
    // 当我们调用testWork方法时,直接传入一个实现了ICalculate接口的匿名内部类即可
    // 该匿名内部类,可以灵活的实现work方法,完成不同的计算任务
    public void testWork(ICalculate iCalculate, double n1, double n2) {
        double result = iCalculate.work(n1, n2);// 动态绑定
        System.out.println(result);
    }
}

匿名内部类涉及的知识点

成员内部类

说明

  1. 成员内部类 是定义在 外部类的成员位置,并且没有static修饰

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

  3. 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员

  4. 作用域和外部类的其他成员一样,为整个类体,比如前面案例,在外部类的成员方法中创建成员内部类对象,再调用方法。

  5. 成员内部类 --- 访问 -----> 外部类(比如:属性) 【访问方式:直接访问】

  6. 外部类 --- 访问 -----> 内部类 【访问方式:创建对象,再访问】

  7. 外部其他类 --- 访问 -----> 成员内部类 【3种方式】

    • // 方式一
      // outer.new Inner(); 相当于把 new Inner()当做是outer的成员
      // 这就是一个语法,不要特别的纠结
      Outer outer = new Outer();
      Outer.Inner inner = outer.new Inner();
      // 方式二 在外部类中,编写一个方法,可以直接返回一个实例对象
      public Inner getInnerInstance() {
          return new Inner();
      }
      // 方式三 和第一种方式类似
      Outer.Inner inner = new Outer().new Inner();
      
  8. 如果外部类和内部类的成员重名时,内部类访问的话,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问。

静态内部类

说明

  1. 静态内部类是定义在外部类的成员位置,并且有static修饰

  2. 可以直接访问外部类的所有静态成员包括私有的,但是不能直接访问非静态成员

  3. 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员

  4. 作用域:同其他的成员一样,为整个类体

  5. 静态内部类 --- 访问 ---> 外部类(比如:静态属性)【访问方式:直接访问所有静态成员】

  6. 外部类 --- 访问 -----> 静态内部类【访问方式:创建对象,再访问】

  7. 外部其他类 --- 访问 ----> 静态内部类【2种】

    • // 方式一
      // 因为静态内部类,是可以通过类名直接访问(前提是满足访问权限)
      Outer.Inner inner = new Outer.Inner();
      innner.say();
      // 方式二
      // 编写一个方法,可以返回一个静态内部类的实例
      public Inner getInner() {// 非静态方法
          return Inner();
      }
      public static Inner getInnerInstance() {// 静态方法
          return Inner();
      }
      
  8. 如果外部类和静态内部类的成员重名时,静态内部类访问外部成员时默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.成员)去访问。

枚举(enumeration)

基本介绍

  1. 枚举对应英文(enumeration, 简称 enum)
  2. 枚举是一组常量的集合
  3. 可以这样理解:枚举属于一种特殊的类,里面只包含一组 有限 的特定的对象

枚举的二种实现方式

  1. 自定义类实现枚举。
  2. 使用enum 关键字实现枚举。

自定义枚举

实现步骤

  1. 将构造器私有化,目的是防止 直接 new
  2. 在类内部,直接创建固定的一组对象. public static .
  3. 对外暴露对象 (通过为对象添加 public static final修饰符)
  4. 优化,可以加入 final 修饰符 final.
  5. 可以提供getXxx方法
  6. 去掉setXxx方法,防止属性被修改.

细节

  1. 不需要提供setXxx方法,因为枚举对象值通常为只读
  2. 对枚举对象/属性使用 final + static 共同修饰,实现底层优化。
  3. 枚举对象名通常使用全部大写,常量的命名规范。
  4. 枚举对象根据需要,也可以有多个属性。

演示

class Season {
    private String name;// 季节名称
    private String desc;// 季节描述
    public static final int i = 10;
    public static final Season SPRING = new Season("春天", "温暖");
    public static final Season SUMMER = new Season("夏天", "炎热");
    public static final Season AUTUMN = new Season("秋天", "凉爽");
    public static final Season WINTER = new Season("冬天", "寒冷");
    public Season(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
    public String getName() {
        return name;
    }
    public String getDesc() {
        return desc;
    }
}

用 enum 关键字实现枚举

使用 enum 来实现枚举类

  1. 使用关键字 enum 替代 class.

  2. 由原来的public static final Season SPRING = new Season("春天", "温暖");

    现在直接使用 SPRING("春天", "温暖"); 常量名(实参列表);.

  3. 如果有多个常量(对象)使用,(逗号)间隔即可。

  4. 如果使用 enum 来实现枚举,要求将定义的常量对象,写在最前面

// 示例代码
enum  Season1 {
    SPRING("春天", "温暖"), SUMMER("夏天", "炎热")
    , AUTUMN("秋天", "凉爽"),WINTER("冬天", "寒冷");
    private String name;// 季节名称
    private String desc;// 季节描述
    private Season1(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
}

注意事项

  1. 当我们使用 enum 关键字开发一个枚举类时,默认会继承Enum 类而且是一个final 类【如何证明】,使用javap工具来演示.

Show in Explorer 打开资源管理器.

  1. 传统的 public static final Season SPRING = new Season("春天", "温暖");

    简化成 SPRING("春天", "温暖"); 这里必须知道,它调用的是那个构造器。

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

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

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

Enum类

Enum常用方法

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

// 看下源码定义
public abstract class Enum<E extends Enum<E>>
    implements Comparable<E>,Serializable {
}
  1. toString():Enum类已经重写过了,返回的是当前对象名. 子类可以重写该方法,用于返回对象的属性信息

  2. name()输出当前枚举对象的名称(常量名),子类不能重写。

  3. ordinal()输出的是枚举对象的次序/编号,从 0 开始编号.

  4. values():从反编译可以看出 values 方法,返回对象数组,对象数组含有定义的所有枚举对象

  5. valueOf()将字符串转换成枚举对象,要求字符串必须为已有的常量名,否则报异常!

    valueOf()的执行流程:

    1. 根据你输入的 字符串 到 枚举对象 中查找.

    2. 如果找到了,就返回,如果没有找到,就报错.

  6. compareTo()比较两个枚举常量(对象),比较的就是编号。

增强for循环

for (数据类型 变量名 : 数组名) {// 注意 : m
    System.out.println(变量名);
}
int[] nums = {1, 2, 5, 3};
// 普通for循环
for (int i = 0; i < nums.length; i++) {
    System.out.println(nums[i]);// 1 2 5 3
}
// 增强for循环
// 执行流程是 依次从nums数组中取出数据,赋给i,如果取出完毕,则退出for
for (int i : nums) {
    System.out.println(i);// 1 2 5 3
}

注意和细节

  1. 使用 enum 关键字后,就不能再继承其它类了,因为enum会隐式继承Enum,而java是单继承机制.

  2. 枚举类和普通类一样,可以实现接口,如下形式:

    enum 类名 implements 接口1, 接口2{}
    
  3. // 枚举在switch中的使用
    // switch后的 ()中,放入枚举对象
    // 在每个case后,直接写上在枚举类中定义的枚举对象即可
    Color color = Color.RED;
    switch (color) {
        case RED:
            System.out.println("红色");
            break;
        case YELLOW:
            System.out.println("黄色");
            break;
        default:
            System.out.println("没有找到...");
            break;
    }
    

注解(Annotation)

基本介绍

  1. 注解(Annotation) 也被称为 元数据(Metadata),用于修饰解释 包、类、方法、实现、构造器、局部变量等数据信息。
  2. 和注释一样,注解不影响程序逻辑但注解可以被编译或运行,相当于嵌入在代码中的补充信息。
  3. 在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码XML配置等。

基本分类

使用Annotation 时要在其前面增加 @ 符号,并把该 Annotation 当成一个修饰符使用。用于修饰它支持的程序元素.

Deprecated 过时.

Suppress 抑制.

三个基本的 Annotation

  1. @Override:限定某个方法,是重写父类方法,该注解只能用于方法.
  2. @Deprecated:用于表示某个程序元素(类,方法等)已过时.
  3. @SuppressWarnings抑制编译器警告.

@Override

基本介绍

  1. @Override:限定某个方法,是重写父类方法,该注解只能用于方法。
  2. 补充说明:@interface 的说明:@interface 不是 interface,而是注解类 是jdk5.0之后加入的.

示例代码

class Father {
    public void say() {
        System.out.println("Father say()...");
    }
}
class Son extends Father {
    // 解读
    // 1.@Override 注解放在say()方法上,表示子类的say()方法是重写了父类的say()
    // 【这里的父类不限于直接父类】
    // 2.这里如果没有写@Override 也还是重写了父类的say()
    // 3.如果你写了@Override注解,编译器就会去检查该方法是否真的重写了父类的方法,
    //   如果的确重写了,则编译通过,如果没有构成重写,则编译错误
    // 4.看看@Override的定义
    //   解读:@interface 表示一个 注解类
    /*
        @Target(ElementType.METHOD)
        @Retention(RetentionPolicy.SOURCE)
        public @interface Override {
        }
    */
    @Override
    public void say() {
        System.out.println("Son say()...");
    }
}

使用说明

  1. @Override 表示指定重写父类的方法(从编译层面验证),如果父类没有say()方法,则会报错。
  2. 如果不写 @Override 注解,而父类仍有 public void say(){} ,,仍然构成重写。
  3. @Override 只能修饰方法,不能修饰其它类,包,属性等等。
  4. 查看 @Override 注解源码为 @Target(ElementType.METHOD),说明只能修饰方法。
  5. @Target 是修饰注解的注解,称为元注解.

@Deprecated

基本介绍

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

使用说明

LOCAL_VARIABLE 局部变量.

  1. 用于表示某个程序元素(类,方法等)已过时。
  2. 可以修饰方法,类,字段,包,参数 等等。
  3. @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
  4. @Deprecated 的作用可以做到新旧版本的兼容和过渡.

示例代码

// 解读
// 1.@Deprecated 修饰某个元素,表示该元素已经过时
// 2.即不推荐使用,但是仍然可以使用
// 3.查看 @Deprecated 注解类的源码
// 4.可以修饰方法,类,字段,包,参数 等等
// 5.@Deprecated 可以做版本升级过渡使用
/*
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, 		TYPE})
    public @interface Deprecated {
    }
*/
@Deprecated
class A {
    @Deprecated
    public int n1 = 10;
    @Deprecated
    public void ok() {}
}

@SuppressWarnings

基本介绍

@SuppressWarnings:抑制编译器警告。

使用说明

  1. unchecked 是忽略没有检查的警告

  2. rawtypes 是忽略没有指定泛型的警告(传参时没有指定泛型的警告错误)

  3. unused 是忽略没有使用某个变量的警告错误

  4. @SuppressWarnings 可以修饰的程序元素为:查看@Target.

  5. 生成 @SuppressWarnings 时,不用背,直接点击左侧的黄色提示,就可以选择(注意可以指定生成的位置)

  6. // 参数 String[] 字符串数组 只能使用 {"all"} 的形式,不能用 new String("all"); 因为 看下面
    // Attribute value must be constant // 属性值必须为常量
    @SuppressWarnings({"all"})
    

[SuppressWarning 中的属性介绍以及属性说明.txt](./.~SuppressWarning 中的属性介绍以及属性说明.txt).

示例代码

// 解读
// 1.当我们不希望看到这些警告的时候,可以使用 @SuppressWarnings注解来抑制警告信息
// 2.在{""} 中,可以写入你希望抑制(不显示)的警告信息
// 3.可以指定的警告有--》看上面的文章 SuppressWarning 中的属性介绍以及属性说明.txt
// 4.关于@SuppressWarnings 作用范围是和你放置的位置相关
//   比如@SuppressWarnings 放置在main方法,那么抑制警告的范围就是 main
//   通常我们可以放在具体的语句,方法,类
// 5.看看 @SuppressWarnings 的源码
// 5.1 放置的位置就是:TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE
// 5.2 该注解类有数组 String[] value() 设置一个数组比如 {"unchecked", "rawtypes", "unused"}
/*
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
*/
public static void main(String[] args) {
    List list = new ArrayList();
    list.add("jack");
    list.add("tom");
    list.add("marry");
    int i;
    System.out.println(list.get(1));
}

元注解(了解)

元注解基本介绍

JDK 的元Annotation 用于修饰其他 Annotation

元注解:本身作用不大,讲这个原因希望同学们,看源码时,可以知道它是干什么的

元注解种类(使用不多,了解,不用深入研究)

  1. Retention:指定注解的作用范围,三种 SOURCE,CLASS,RUNTIME.
  2. Target:指定注解可以在哪些地方使用.
  3. Documented:指定该注解是否会在 javadoc 体现.
  4. Inherited:子类会继承父类注解.

@Retention 注解

基本说明

Retention 保留.

Policy 策略.

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

@Retention的三种值

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

一个@Retention的案例

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)// 这个就是@Retention的取值
public @interface Override {
}
// 说明:
// Override 的作用域在SOURCE,
// 当编译器编译时生效,不会写入到.class文件,也不会再Runtime(运行时)生效

@Target注解

基本说明

用于修饰 Annotation 定义,用于指定被修饰的Annotation 能用于修饰哪些程序元素. @Target 也包含一个名为 value 的成员变量。

一个@Target的案例

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@Target的源码说明

@Documented
@Retention(RetentionPolicy.RUNTIME)// 它的作用范围是 RUNTIME
@Target(ElementType.ANNOTATION_TYPE)// 这里的ANNOTATION_TYPE 说明@Target只能修饰注解
public @interface Target {// 说明它是注解
    ElementType[] value();// 可以简单看一下ElementType的取值//通过Enum 比如 TYPE等
}

@Documented

基本说明

@Documented:用于指定被该元 Annotation 修饰的 Annotation 类将被 javadoc 工具提取成文档,即在生成文档时,可以看到该注解

看一个@Documented案例

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@Documented 源码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Inherited 注解

基本介绍

被他修饰的 Annotation 将具有继承性.如果某个类使用了被 @Inherited 修饰的 Annotation,则其子类将自动具有该注释。

基本说明

实际应用中,使用较少,了解即可。

小结:本身作用不大,主要是看源码时,可以知道它是干什么的。

异常(Exception)

异常快捷键

使用方法将光标放在(或选中), 可能出现异常的代码块上, 输入快捷键 ctrl+alt+Talt+T 选中 try-catch即可

基本介绍

  • 基本概念

    Java语言中,将程序执行中发生的不正常情况称为 “异常”。(开发过程中的语法错误逻辑错误 不是异常)

  • 执行过程中所发生的异常事件可分为两大类:

    1. Error(错误)Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。比如:StackOverflowError栈溢出】和 OOM(out of memory)内存不足】,Error是严重错误,程序会崩溃
    2. Exception(异常):其它因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。例如:空指针访问,试图读取不存在的文件,网络连接中断等等,Exception 分为两大类运行时异常【程序运行时,发生的异常】和编译时异常【编程时,编译器检查出的异常】。

异常体系图

  • 异常体系图:(蓝实线表示继承,绿虚线表示实现)
image-20211217091015051
  • 异常体系图二
image-20211217092001864
  • 异常体系图小结

    unhandled 未处理.

    1. 异常分为两大类,运行时异常和编译时异常。
    2. 运行时异常,编译器不要求强制处理的异常。一般是指编程时的逻辑错误,是程序员应该避免其出现的异常java.lang.RuntimeException类及它的子类都是运行时异常。
    3. 对于运行时异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。
    4. 编译时异常,是编译器要求必须处理的异常

五大运行时异常

  • 常见的运行时异常包括【五个】
    1. NullPointerException:空指针异常
    2. ArithmeticException:数学运算异常
    3. ArrayIndexOutOfBoundsException:数组下标越界异常
    4. ClassCastException:类型转换异常
    5. NumberFormatException:数字格式不正确异常

NullPointerException 空指针异常

Method invocation 方法调用.

produce 产生.

  • 当应用程序试图 在需要对象的地方使用 null,抛出该异常。

ArithmeticException 数学运算异常

  • 出现异常的运算条件时,抛出此异常。例如,一个整数 “除以零” 时,抛出此类的一个实例。

ArrayIndexOutOfBoundsException 数组下标越界异常

  • 非法索引访问数组时 抛出的异常。如果索引为负大于等于数组大小,则该索引为非法索引
  • 数组的最大索引为 arr.length-1, 最小索引为 0, 数组长度(大小)为 arr.length.
image-20211217101201674

ClassCastException 类型转换异常

  • 当试图将对象强制转换为不是实例的子类时,抛出该异常。
// 示例一
Object o = "hello";
Integer i = (Integer) o;// ClassCastException  String --> Integer
// 示例二
class A {}
class B extends A {}
class C extends A {}
// main方法
A a = new B();// ok 向上转型
B b = (B)a;// ok
C c = (C)a;// ClassCastException  B --> C

NumberFormatException 数字格式不正确异常

  • 当应用程序试图 将字符串转换成一种数据类型但该字符串不能转换为适当格式时,抛出该异常 ==》使用异常我们可以确保输入是满足条件的数值
int i = Integer.parseInt("hello");// NumberFormatException
int i = Integer.parseInt("123");// ok

编译异常

基本介绍

  • 编译异常是指在编译期间,就必须处理的异常,否则代码不能通过编译。

常见的编译异常

  1. SQLException:操作数据库时,查询表可能发生异常。
  2. IOException:操作文件时,发生的异常。
  3. FileNotFoundException:当操作一个不存在的文件时,发生异常。
  4. ClassNotFoundException:加载类,而该类不存在时,异常。
  5. EOFException:操作文件,到文件末尾,发生异常。
  6. IllegalArgumentException:参数异常。

异常处理

基本介绍

  • 异常处理就是当异常发生时,对异常处理的方式

异常处理方式

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

  2. throws:将发生的异常抛出,交给调用者(方法)来处理,最顶级的处理者就是 JVM.

示意图

try-catch-finally示意图
image-20211217120008718
throws示意图
image-20211217114759667

try-catch方式处理异常

catch 捕获.

基本说明

  1. Java提供try 和 catch 块来处理异常try块用于包含可能出错的代码catch块用于处理try块中发生的异常。可以根据需要在程序中有多个 try...catch块

  2. 基本语法

    try {
        // 可疑代码
        // 将异常生成对应的异常对象,传递给catch块
    } catch (异常) {
        // 对异常的处理
    }
    // 如果没有finally,语法是可以通过的
    // 有catch,就会捕获异常,就不会导致系统崩溃
    

注意事项

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

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

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

    try {
        // 可疑代码
    } catch (异常) {
        // 处理..
    } finally {
        // 释放资源等...
    }
    
  4. 可以有多个catch语句,捕获不同的异常(进行不同的业务处理)要求父类异常在后子类异常在前,比如(Exception 在后,NullPointerException 在前),如果发生异常,只能匹配一个catch,案例演示

    try {
    } catch (NullPointerException e) {
    } catch (Exception e) {
    } finally {
    }
    
  5. 可以进行 try-finally 配合使用,这种用法相当于没有捕获异常,因此程序会直接崩溃应用场景,就是执行一段代码,不管是否发生异常,都必须执行某个业务逻辑.

    try {
    } finally {
    }
    
  6. 如果没有catch,出现异常时就会导致程序直接崩溃,不会执行后续代码。有catch 时,不会导致程序直接崩溃,会执行后续代码

课堂练习

objects of inconvertible types 不可转换类型的对象.

convert 转换.

Condition 条件.

Contents 内容.

complete normally 正常完成.

image-20211219095835388.

// 1.在finally中return
// 执行顺序
// 1.try中语句,发生异常之前的语句
// 2.catch中的语句,return语句当做普通语句(因为finally中有return)
// 3.finally中的语句,最终由finally中的return关键字返回相应的值
public class Exception01 {
    // main方法
    public static void main(String[] args) {
        System.out.println(method());
    }
    public static int method() {
        try {
            // 这里names 指向一个String[]数组
            // 但是没有向数组中赋值,数组中的值都是默认值 null
            String[] names = new String[3];
            if (names[1].equals("tom")) {// NullPointerException
                System.out.println(names[1]);
            }
        } catch (NullPointerException e) {// 捕获
            return 3;// 当做普通语句处理,这里不直接返回3,因为有finally
        } finally {// 必须执行,即使之前有return 也会执行.
            return 4;// 最终返回4
        }
    }
}
// 2.在finally中return,catch中的return语句当做普通语句来处理
// 执行顺序 同上
public class Exception02 {
    public static void main(String[] args) {
        System.out.println(method());
    }
    public static int method() {
        int i = 1;
        try {
            i++;// i = 2
            // 这里names 指向一个String[]数组
            // 但是没有向数组中赋值,数组中的值都是默认值 null
            String[] names = new String[3];
            if (names[1].equals("tom")) {// NullPointerException
                System.out.println(names[1]);
            }
        } catch (NullPointerException e) {// 捕获
            return ++i;// i = 3
        } finally {// 必须执行
            return ++i;// i = 4 最终返回4
        }
    }
}
// 3.finally中没有return,就往上找(1)有返回值return的(2)且能返回的语句,这里返回临时变量的值!!!
// 执行顺序(发生异常)
// 1.try中语句,发生异常之前的语句
// 2.catch中的语句,这里return也会当做普通语句来执行,此时底层也会用临时变量保存return关键字返回的值
// 3.finally中的语句
// 4.执行catch中 return 临时变量值;
public class Exception03 {
    public static void main(String[] args) {
        System.out.println(method());
    }
    public static int method() {
        int i = 1;
        try {
            i++;// i = 2
            // 这里names 指向一个String[]数组
            // 但是没有向数组中赋值,数组中的值都是默认值 null
            String[] names = new String[3];
            if (names[1].equals("tom")) {// NullPointerException
                System.out.println(names[1]);
            }
            return 1;
        } catch (NullPointerException e) {// 捕获
            return ++i;// i = 3 => 底层用临时变量保存 temp = 3 最终返回一个temp=3
        } finally {// 必须执行
            ++i;// i = 4
            System.out.println("i=" + i);// i = 4
        }
    }
}
// 3.finally中没有return,就往上找(1)有返回值return的(2)且能返回的语句,这里返回临时变量的值!!!
// 执行顺序(无异常发生)
// 1.try中语句,这里return也会当做普通语句来执行,此时底层也会用临时变量保存 return关键字返回的值
// 2.finally中的语句
// 3.执行try中 return 临时变量值;
public class Exception03 {
    public static void main(String[] args) {
        System.out.println(method());
    }
    public static int method() {
        int i = 1;
        try {
            i++;// i = 2
            return ++i;// i = 3 => 底层用临时变量保存 temp = 3 最终返回一个temp=3
        } catch (Exception e) {// 没有异常,不捕获
            return ++i;// 该语句不执行
        } finally {// 必须执行
            ++i;// i = 4
            System.out.println("i=" + i);// i = 4
        }
    }
}
// 总结:
// 1.先不管是否为return语句,都按普通语句处理(return语句也按普通语句处理)
// 2.正常执行程序
// 3.最终return按 finally-》catch -》try的顺序查找(从后往前找),
//   catch中的return语句要看是否执行过,没有执行过就不算
// 4.return关键字返回的值为 底层用临时变量保存的值/(底层用临时变量保存的return表达式的值)

try-catch-finally执行顺序小结

  1. 如果没有出现异常,则执行try块中所有语句不执行catch块中语句,如果有finally,最后还需要执行finally里面的语句.
  2. 如果出现异常,则try块中异常发生后,try块剩下的语句不再执行将执行catch块中的语句,如果有finally,最后还需要执行finally里面的语句.

try-catch最佳实践

// 版本1: 递归版(非常不推荐,容易栈溢出)
public int inputIntNum() {
    Scanner myScanner = new Scanner(System.in);
    try {
        System.out.println("请输入一个整数:");
        return Integer.parseInt(myScanner.next());
    } catch (NumberFormatException e) {
        System.out.println("输入有误!!");
        return inputIntNum();
    }
}
// 版本2: flag版
public int inputIntNum() {
    Scanner myScanner = new Scanner(System.in);
    boolean flag = true;// 退出标志
    int res = -1;
    while (flag) {
        try {
            System.out.println("请输入一个整数:");
            res = Integer.parseInt(myScanner.next());
            flag = false;
        } catch (NumberFormatException e) {
            System.out.println("输入有误!!");
        }
    }
    return res;
}
// 版本3:无限循环版(推荐)
// while(true) + break;组合
// return语句写在循环外部
public int inputIntNum() {
    Scanner myScanner = new Scanner(System.in);
    int res = -1;
    while (true) {
        try {
            System.out.println("请输入一个整数:");
            res = Integer.parseInt(myScanner.next());
            break;
        } catch (NumberFormatException e) {
            System.out.println("输入有误!!");
        }
    }
    return res;
}

throws异常处理

throws 抛出.

NotNull 非空.

基本介绍

Surround with try/catch 用try/catch 包围.

Surround 包围,围绕.

Add exception to method signature 向方法签名中添加异常.

signature 签名.

  1. 如果一个方法(中的语句执行时)可能生成某种异常,但是并不能确定如何处理这种异常,则此方法应显示地声明抛出异常表明该方法将不对这些异常进行处理,而由该方法的调用者(方法)负责处理
  2. 在方法声明中用throws语句可以声明抛出异常列表 ( 即可以抛出多个异常,用逗号分隔),throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类

注意事项和细节

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

自定义异常(CustomException)

基本概念

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

自定义异常的步骤

  1. 定义类:自定义异常类名(程序员自己写) 继承 ExceptionRuntimeException.
  2. 如果继承Exception,属于编译异常.
  3. 如果继承RuntimeException,属于运行异常
  4. (一般情况下,我们自定义异常是 继承RuntimeException:即把自定义异常做成 运行时异常,好处是,我们可以使用默认的异常处理机制throws,比较方便)

基本案例

// 自定义异常
class AgeException extends RuntimeException {
    public AgeException(String message) {// 构造器
        super(message);
    }
}
// 案例一:运行时异常 进行默认的throws处理
// main方法
    int age = 180;     
    if (!(age > 18 && age < 150)) {
        throw new AgeException("年龄范围不对");// 抛出一个异常 给JVM
    }
    System.out.println("年龄范围正确");

// 案例二:运行时异常 用显式的try-catch处理异常
// main方法
    int age = 0;
    try {
        age = 180;
        if (!(age > 18 && age < 150)) {
            throw new AgeException("年龄范围不对");// 抛出一个异常 给catch
        }
        System.out.println("年龄范围正确");
    } catch (AgeException e) {// 捕获到 AgeException异常
        System.out.println(e.getMessage());
    }

throw VS throws

意义 位置 后面跟的东西
throws 异常处理的一种方式 方法声明处 异常类型
throw 手动生成异常对象关键字 方法体中 异常对象

throw 执行顺序(对运行时异常而言)

  1. 首先看方法体内是否有相应的catch 来捕获
  2. 如果没有catch 则执行默认的异常处理机制throws,抛给调用者来处理
  3. 在按1、2的步骤继续来看
public class ThrowException {
    public static void main(String[] args) {
        try {
            System.out.println("不管checkAge()是否有异常抛出都会执行");
            checkAge();
            System.out.println("只有当checkAge()没有异常抛出时才会执行,有异常抛出时不会执行");
        } catch (AgeException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("程序继续...");
    }

    public static void checkAge() {// 检查年龄是否正确
        try {
            int age = 100;
            if (age < 18 || age > 150) {
                throw new AgeException("年龄范围有误!");
            }
            System.out.println("年龄正确");
        } catch (NullPointerException e) {// throw的异常 与 catch的异常 没有关系 
            System.out.println(e.getMessage());
        }
    }
}
// 自定义AgeException异常
class AgeException2 extends RuntimeException {
    public AgeException2(String message) {
        super(message);
    }
}

异常课后练习

Required type 所需类型.

Provided 提供.

// 1.编写程序 HomeWork01.java,接收命令行的两个参数(整数),计算两数相除
// 2.计算两个数相除,要求使用方法 cal(int n1, int n2)
// 3.对数据格式不正确(NumberFormatException)、缺少命令行参数(ArrayIndexOutOfBoundsException)、除0进行异常处理(ArithmeticException)
// 示例代码
public class HomeWork05 {
    public static void main(String[] args) {
        try {
            // 先验证输入的参数的个数是否正确 两个参数
            if (args.length != 2) {
                throw new ArrayIndexOutOfBoundsException("命令行参数个数不正确");
            }
            // 先把接收到的参数,转成整数
            int n1 = Integer.parseInt(args[0]);
            int n2 = Integer.parseInt(args[1]);
            
            double res = cal(n1, n2);// 该方法可能抛出ArithmeticException
            System.out.println(res);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println(e.getMessage());
        } catch (NumberFormatException e) {
            System.out.println("数据格式不正确");
        } catch (ArithmeticException e) {
            System.out.println("除数不能为0");
        }
    }
    // 编写cal方法,求两个数的商
    public static double cal(int n1, int n2) {
        return n1 / n2;
    }
}

常用类

八大包装类(Wrapper)

Wrapper 包装类.

包装类的分类

  1. 针对八种基本数据类型相应的引用类型—包装类.
  2. 有了类的特点,就可以调用类中的方法。
基本数据类型 包装类 父类
boolean Boolean Object
char Character Object
byte Byte Number -> Object
short Short Number -> Object
int Integer Number -> Object
long Long Number -> Object
float Float Number -> Object
double Double Number -> Object

示意图:

image-20211221175833290.

image-20211221175939368.

image-20211221180217377.

装箱 和 拆箱

  • 包装类和基本数据类型的转换(以 int 和 Integer为例演示)

    1. jdk5 前是手动装箱和拆箱的方式,装箱:基本数据类型 ==》包装类型;反之:拆箱
    2. jdk5 以后(含jdk5) 是自动装箱和拆箱的方式
    3. 自动装箱底层调用的是 valueOf 方法,比如 Integer.valueOf().
    4. 其他包装类的用法类似,不一一举例。
    // 示例代码
    // 演示 int <--> Integer 的装箱和拆箱
            // jdk5前是手动装箱和拆箱
            // 手动装箱 int -> Integer
            int n1 = 100;
            Integer integer1 = new Integer(n1);
            Integer integer2 = Integer.valueOf(n1);
            // 手动拆箱 Integer -> int
            int i1 = integer1.intValue();
    
            // jdk5后,就可以自动装箱和拆箱
            // 自动装箱 int -> Integer
            int n2 = 100;
            Integer integer3 = n2;// 自动装箱:底层使用的是 Integer.valueOf(n2);
            // 自动拆箱 Integer -> int
            int i2 = integer2;// 自动拆箱:底层使用的是 integer2.intValue();
    

经典面试题

simplified 简化.

  • 三元运算符 是一个整体,会提升优先级
  • if else 不是一个整体,if 和 else 分别计算、相互独立。
// 三元运算符是一个整体,这里会提升优先级,提升为double类型,因此输出 1.0
// “一真大师”
    Object obj1 = true? new Integer(1) : new Double(2.0);
    System.out.println(obj1);// 1.0
// if else 分别计算、相互独立,这里不会提升优先级,因此输出 1
    Object obj2;
    if (true) {
        obj2 = new Integer(1);
    } else {
        obj2 = new Double(2.0);
    }
    System.out.println(obj2);// 1

包装类方法

包装类型 和 String类型相互转换

// 包装类型 --> String类型(三种方式)
    Integer i = 100;// 自动装箱
    // 方式一: 加一个空串
    String str1 = i + "";
    // 方式二: 调用包装类的toStirng()方法
    String str2 = i.toString();
    // 方式三: 用String.valueOf()方法
    String str3 = String.valueOf(i);

// String --> 包装类型(两种方式)
    String str4 = "1234";
    // 方式一: 用包装类对应的parseXxx()方法
    Integer i2 = Integer.parseInt(str4);// 使用自动装箱
    // 方式二: 用包装类的构造器
    Integer i3 = new Integer(str4);// 构造器

Integer类 和 Character类的常用方法

// Integer类
    System.out.println(Integer.MIN_VALUE);// 返回最小值
    System.out.println(Integer.MAX_VALUE);// 返回最大值

// Character类常用方法
    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'));// 转成小写

Integer 创建机制

    // 示例一
	// new 在堆中创建对象,因此i 和 j一定不是同一个对象
	// 只要是 new 那就是不同的对象!!
    Integer i = new Integer(1);
    Integer j = new Integer(1);
    System.out.println(i == j);// False
	// 示例二
    // 这里主要是看范围
    // 范围若在 -128~127之间(包含-128和127) 就直接返回
    // 否则,就 new Integer(xx);
    Integer m = 1;// 底层自动装箱 Integer.valueOf(1); -> 阅读源码
    Integer n = 1;// 底层自动装箱 Integer.valueOf(1);
    System.out.println(m == n);// True
    // 这里主要是看范围
    // 范围若在 -128~127之间(包含-128和127) 就直接返回
    // 否则,就 new Integer(xx);
    Integer x = 128;// 底层自动装箱 Integer.valueOf(128);
    Integer y = 128;// 底层自动装箱 Integer.valueOf(128);
    System.out.println(x == y);// 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 i1 = 127;
	int i2 = 127;
	System.out.println(i1 == i2);// True
	// 只要有基本数据类型,判断的就是值是否相等
	int i3 = 128;
	Integer i4 = 128;
	System.out.println(i3 == i4); // True

String类

String结构剖析

String类示意图

image-20211223160627167.

String类的理解和创建对象

  1. String 对象用于保存字符串,也就是一组字符序列.

  2. 字符串常量对象是用双引号括起的字符序列。例如 "jack","你好","12.7", "boy".

  3. 字符串的字符使用Unicode字符编码,一个字符(不区分字母还是汉字)占两个字节。

  4. String类有很多构造器,构造器的重载

    // 常用的构造器
    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);
    
  5. String类 实现了接口 Serializable【String 可以串行化:可以在网络传输】

  6. String类也实现了接口 Comparable【String 对象可以比较大小】

  7. Stringfinal 类,不能被其它类继承

  8. String有属性 private final char value[]; 用于存放字符串内容

  9. 一定要注意:value 是一个 final 类型,不可以修改【需要功力】: 即value不能指向新的地址,但是单个字符内容是可以变化的.

    final char[] value = {'a', 'b', 'c'};// final修饰的char数组
    value[0] = 'H';// ok
    char[] v2 = {'t', 'o', 'm'};
    value = v2;// 错误,不可以修改 value的地址
    char[] v3 = {'j', 'a', 'c', 'k'};
    v3 = v2;// ok
    

String创建剖析

创建String对象的两种方式

  1. 方式一:直接赋值 String s = "hsp";
  2. 方式二:调用构造器 String s2 = new String("hsp");

两种创建String对象的区别

  • 方式一:直接赋值 String s1 = "hsp";
    • 方式一:先从常量池查看是否有"hsp" 数据空间,如果有,直接指向;如果没有则重新创建,然后指向s1 最终指向的是常量池的空间地址
  • 方式二:调用构造器 String s2 = new String("hsp");
    • 先在堆中创建空间,里面维护了value属性,指向常量池的hsp空间。如果常量池没有"hsp",重新创建,如果有,直接通过value指向s2 最终指向的是堆中的空间地址

内存分布图

image-20211223173936360.

练习题

// 
String a = "abc";
String b = "abc";
System.out.println(a.equals(b));// True String类已经重写了equals()方法,这里比较的是内容是否一样,这里内容一样,故为True
System.out.println(a == b);// True == 比较是否是同一个对象,即比较对象的地址是否相等,这里都是指向,这里都指向常量池中"abc"的空间地址,故为True
String a = "hsp";// a指向 常量池的"hsp"
String b = new String("hsp");// b指向堆中对象
System.out.println(a.equals(b));// 比较内容 True
System.out.println(a == b);// False
// b.intern() 方法返回常量池的地址
System.out.println(a == b.intern());// True
System.out.println(b == b.intern());// False
/*  知识点
	当调用intern方法时,如果池(常量池)已经包含一个等于此String对象的字符串(用equals(Object)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用
	解读:(1)b.intern()方法最终返回的是常量池的地址(对象)
*/
Person p1 = new Person();
p1.name = "hspedu";
Person p2 = new Person();
p2.name = "hspedu";
System.out.println(p1.name.equals(p2.name));// 比较内容:True
System.out.println(p1.name == p2.name);//指向同一个"hspedu"字符串常量 True
System.out.println(p1.name == "hspedu");// "hspedu"本身就在池中,不是new出来的,这个地方返回的地址就是池中的地址,因为"hspedu"是字符串常量 True

String s1 = new String("bcde");
String s2 = new String("bcde");
System.out.println(s1 == s2);//s1,s2都是new出来的,不是同一个对象 故为False (但是s1,s2对象中的属性value都是指向池中的"bcde")

String对象特性

  1. String是一个final类(不能 被继承),代表不可变的字符序列。
  2. 字符串是不可变的。一个字符串对象一旦被分配,其内容是不可变的。
  3. 但字符串引用可以重新赋值。
//1.以下语句创建了几个对象?2个对象
String s1 = "hello";
s1 = "haha";// 不是把"hello"修改为"haha",而是重新创建一个"haha"对象,然后s1指向"haha"

// 以下语句创建了几个对象? 只有 1个对象
String a = "hello" + "abc";
//解读:String a = "hello" + "abc"; 编译器不傻,底层会优化 等价于 String a = "helloabc";
// 小结:
// 1.编译器不傻,底层会做一个优化,判断创建的常量池对象,是否有引用指向
// 2.String a = "hello" + "abc"; ==优化成==》String a = "helloabc";

// 以下语句创建了几个对象? 一共有3个对象
String a = "hello";// 创建 a对象
String b = "abc";// 创建 b对象
String c = a + b;
String d = "helloabc";
System.out.println(c == d);// False
String e = "hello" + "abc";// 直接看池,e指向常量池
System.out.println(d == e);// True
// 关键就是要分析 String c = a + b;到底是如何执行的
// String c = a + b;解读
// 1.先 创建一个 StringBuilder sb = new StringBuilder();
// 2.执行 sb.append("hello");
// 3.执行 sb.append("abc");
// 4. String c = sb.toString()
//最后其实是 c 指向堆中的对象(String), 该对象的value[]->池中"helloabc"
// 自己动手画内存图

// 小结
// 底层是 StringBuilder sb = new StringBuilder();
// sb.append(a); sb.append(b); sb是在堆中,并且append是在原来字符串的基础上追加的.
// 重要规则:
// String c1 = "ab" + "cd"; 常量相加,看的是池
// String c2 = a + b; 变量相加,看的是堆

//学习思路:一定尽量看源码学习 
String s1 = "hsp";// s1指向池中"hsp"
String s2 = "java";// s2指向池中"java"
String s5 = "hspjava";// s5指向池中"hspjava"
String s6 = (s1+s2).intern();// s6指向池中"hspjava"
System.out.println(s5 == s6);// True
System.out.println(s5.equals(s6));// True
public class Test1 {
    String str = new String("hsp");
    final char[] ch = {'j', 'a', 'v', 'a'};
    public void change(String str, char[] ch) {
        str = "java";
        ch[0] = 'h';
    }
    public static void main(String[] args) {
        Test1 ex = new Test1();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");
        System.out.println(ex.ch);//最终输出hsp and hava  这里自己解决
        //System.out.println(ex.str + "and" + ex.ch);// 输出 hsp and 一个地址
    }
}

草稿本-3(1).

String类的常见方法

基本说明

  • String类是保存字符串常量的。每次更新都需要重新开辟空间,效率较低,因此java设计者还提供了 StringBuilderStringBuffer 来增强 String的功能,并提高效率。

常用方法

// equals: 区分大小写,判断内容是否相同
// equalsIgnoreCase: 忽略大小写,判断内容是否相同
// length: 获取字符的个数,字符串的长度
// indexOf: 获取字符(或字符串)在字符串中第1次出现的索引,索引从0开始,如果找不到,返回-1
// laseIndexOf: 获取字符(或字符串)在字符串中最后1次出现的索引,索引从0开始,如找不到,返回-1
// subString: 截取指定范围的子串
// trim: 去前后空格, 字符串间的空格无法去掉
// charAt: 获取某索引处的字符,注意不能使用Str[index] 这种方式

// String str = "hello";
// str[0];// 大错特错
// str.charAt(0) => 'h'
String s1 = "we@terwe@g";
System.out.println(s1.indexOf('w'));// 0 
System.out.println(s1.indexOf("e@"));// 1 返回字符串第一次匹配成功的首个字符的Index
System.out.println(s1.lastIndexOf('w'));// 6
System.out.println(s1.lastIndexOf("e@"));// 7 返回字符串最后一次匹配成功的首个字符的Index
String s2 = "hello,张三";
// s2.substring(6) 从索引6开始截取后面所有的内容
System.out.println(s2.substring(6));// 张三
// s2.substring(1, 5) 从索引1开始截取,截取到 5-1=4位置
// 小技巧:
// 1.前闭后开:字符串截取包括1, 但是不包括5 [beginIndex, endIndex) 
// 2.截串长度:截取长度为 5-1=4, len = endIndex-beginIndex
System.out.println(s2.substring(1, 5));// ello
// toUpperCase:全部转换成大写
// toLowerCase:全部转换成小写
// concat:拼接字符串
// replace:替换字符串中的字符
// str.replace("oldStr", "newStr");
// split:分割字符串,对于某些分割字符,我们需要 转义比如|\\等
// compareTo:比较两个字符串的大小
// toCharArray:转换成字符数组
// format:格式字符串,%s 字符串 %c字符 %d整型 %.2f浮点型
// replace:替换字符串中的字符
// str.replace("oldStr", "newStr");
String s1 = new String("A and B or B");
// 在s1中,将所有的 B 替换成 C
// s1.replace()执行后,返回的结果才是替换过的
// 注意 本身对s1没有任何影响
System.out.println(s1 == s1.intern());// false
s1 = s1.replace("B", "C");
System.out.println(s1);// A and C or C
System.out.println(s1 == s1.intern());// true
/*
s2 = s1.replace("B", "C");
System.out.println(s1);// A and B or B
System.out.println(s2);// A and C or C
*/
// split:分割字符串,对于某些分割字符,我们需要 转义比如|\\等
String str = "a,b,c,";
String[] split = str.split(",");
System.out.println(split.length);// 3 最后一个,后面什么也没有,那就不分配空间了
String str2 = "a,b,c, ";
String[] split2 = str2.split(",");
System.out.println(split2.length);// 4 最后的空格也算一个元素
// compareTo:比较两个字符串的大小, 如果前者大,则返回正数,后者大,就返回负数,如果相等,返回0
// (1)如果长度相同,并且每个字符也相同,就返回0
// (2)如果长度相同或者不相同,但是在进行比较时,可以区分大小,就返回 if (c1 != c2) {return c1 - c2;}
// (3)如果前面的部分都相同,就返回str1.len-str2.len
// 源码
public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    while (k < lim) {// 注意这里没有 "=" 
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}
// format 格式字符串
// 1.占位符有: %s字符串 %c字符 %d整型 %.2f浮点型
// 2.这些占位符由后面的变量来替换
// 3.%s: 表示后面由 字符串来替换
// 4.%d: 是整数来替换
// 5.%.2f: 表示使用小数来替换,替换后,只会保留小数点两位,并且进行(四舍五入)的处理
// 6.%c: 使用char类型来替换
String format = String.format("%s,%s.%c", names[2], names[0], names[1].toUpperCase().charAt(0));
// 技巧:获取字符串的第一个字符并且转换成大写
// str.toUpperCase().charAt(0); 好于 Character.toUpperCase(str.charAt(0));
// 小Bug
System.out.println("s1前" + (s1 == s1.intern()));// 正确,用括号来提升 == 的优先级,达到我们的要求
System.out.println("s1前" + s1 == s1.intern());// 错误,+ 的优先级大于 == 故先字符串拼接,然后再判断是否是同一对象

[javaAPI文档](jdk api 1.8_google.CHM).

StringBuffer类

StringBuffer类结构剖析

基本介绍

  • java.lang.StringBuffer 代表可变的字符序列,可以对字符串内容进行增删。
  • 很多方法与 String 相同,但是 StringBuffer可变长度的
  • StringBuffer 是一个容器

image-20211227190313448.

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

String VS StringBuffer

  1. String 保存的是字符串常量,里面的值不能更改,每次String类的更新实际上就是更改地址,效率较低。// private final char value[];
  2. StringBuffer 保存的是字符串变量,里面的值可以更改,每次StringBuffer的更新实际上可以更新内容不用每次更新地址,效率较高。// char[] value; //这个放在堆中.

自己画示意图

StringBuffer转换

StringBuffer的构造器

capacity 容量.

// 构造器的使用 -> 解读
// 1.创建一个 大小为 16的char[], 用于存放字符内容(字符序列)
StringBuffer stringBuffer1 = new StringBuffer();
// 2.通过构造器指定 char[] 的大小
StringBuffer stringBuffer2 = new StringBuffer(100);
// 3.通过 给一个String 创建 StringBuffer,
//   char[] 大小就是 str.length() + 16
StringBuffer stringBuffer3 = new StringBuffer("hello");

String --> StringBuffer

// String --> StringBuffer
String str = "hello tom";
// 方式1 使用构造器
// 注意: 返回的才是 StringBuffer对象, 对str 本身没有影响
StringBuffer stringBuffer1 = new StringBuffer(str);
// 方式2 使用append方法
StringBuffer stringBuffer2 = new StringBuffer();
stringBuffer2 = stringBuffer2.append(str);

StringBuffer --> String

// StringBuffer --> String
StringBuffer stringBuffer3 = new StringBuffer("你好 Jack");
// 方式1 使用StringBuffer提供的 toString方法
String s = stringBuffer3.toString();
// 方式2 使用构造器
String s1 = new String(stringBuffer3);

StringBuffer类常见方法

append 追加.

// 增 append
// 删 delete(start, end)
// 1.删除索引为>=start && <end 出的字符.前闭后开
// 2.删除长度为 end-start

// 改 replace(start, end, string); 
// 1.将start---end 间的内容替换掉, 不含end.前闭后开(一般情况都是这样)
// 2.string的长度 和 end-start 没有任何关系,前者可以大于后者,也可以等于,或者小于 并非只能替换相同长度的字符序列
// 3.start索引从0开始,可以看作"指针".

// 查 indexOf //查找子串在字符串第1次出现的索引, 如果找不到返回-1
// 插 insert(index, "XXX");
// 解读: 1.在索引为index的位置插入 "XXX", 原来索引为 index的内容自动后移(包含index内容本身)
//      2.索引index从0开始

// 获取长度 length

StringBuffer练习

// 练习1
String str = null;// ok
StringBuffer sb = new StringBuffer();// ok
sb.append(str);// 需要看源码, 底层调用的是 AbstractStringBuilder 的 appendNull() 源码看下面
System.out.println(sb.length());//4 
System.out.println(sb);// null

// 抛出NullPointerException
StringBuffer sb2 = new StringBuffer(str);// 看底层源码super(str.length() + 16);
System.out.println(sb2);
// appendNull()源码
private AbstractStringBuilder appendNull() {
    int c = count;// 获取原字符序列的长度, 并赋给c
    ensureCapacityInternal(c + 4);// 确保内部容量
    // 这里才是精华之处
    // 解读
    // 1.final类型的 char[] value 其地址一旦确定就不能修改, 但是数组中的值可以修改
    // 2.数组是引用类型, 这里value 和 this.value 指向堆中同一个char[], 这里用value 来修改 this.value中的值。
    // 3.自己画内存布局图....
    final char[] value = this.value; 
    value[c++] = 'n';
    value[c++] = 'u';
    value[c++] = 'l';
    value[c++] = 'l';
    count = c;// 更新字符串的长度
    return this;// 返回当前对象
}
// 练习2
// 价格的小数点前面每三位用逗号隔开
// 123.27 -> 123.27
// 1123.12 -> 1,123.12
// 1122333.2 -> 1,122,333.2
// 查 indexOf //查找子串在字符串第1次出现的索引, 如果找不到返回-1
// 插 insert(index, String str);
// 解读: 在索引为index的位置插入 str, 原来索引为 index的内容自动后移(包括index内容本身)

String str = "1234.67";
// 使用StringBuffer 的insert方法,需要将String 转成StringBuffer,然后使用相关方法进行字符串处理
// String 不可变序列,没有insert方法
StringBuffer sb = new StringBuffer(str);
// 1.找到小数点的索引,然后在该位置的前3位,插入","即可
// 2.为什么是前三位? 因为题目要求每三位用逗号隔开
// 3.那为什么要减3,而不是减2,减4呢? 解读: 这里str.lastIndexOf(".");返回一个"."的索引(类似于指针, 指向"."所在的位置,索引一般都是从0开始的),-3就是意味着 "指针"向前移动3位,此时指向的是2所在的位置.用 StringBuffer的insert方法,就会在2的位置插入",",而2及其后面的内容自动后移。上述所有的过程对str本身没有任何影响
// i > 0; 当i指向大于0的位置才进行插入; 当i指向0或者小于0的位置时,就退出循环, 不需要插入。 
// i -= 3; 向前移动三位(i = i -3; 这种写法太low了)
for (int i = str.lastIndexOf(".")-3; i > 0; i -= 3) {
    sb.insert(i, ",");
}
System.out.println(sb);

StringBuilder类

StringBuilder类结构剖析

基本介绍

  1. 一个可变的字符序列。此类提供一个与 StringBuffer 兼容的API,但不保证同步(StringBuilder 不是线程安全)。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类,因为在大多数实现中,它比StringBuffer 更快。

  2. 在 StringBuilder 上的主要操作是 append 和 insert方法,可重载这些方法,以接受任意类型的数据。

  3. 示意图

    image-20211228103904018.

StringBuilder常用方法

  • StringBuilder 和 StringBuffer 均代表可变的字符序列,方法是一样的,所以使用和StringBuffer一样。

StringBuilder源码

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

String Vs StringBuffer Vs StringBuilder

三种比较

synchronized 同步.

  1. StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且方法也一样。

  2. String: 不可变字符序列, 效率低, 但是复用率高.(面试官)

  3. StringBuffer: 可变字符效率, 效率高(增删)、线程安全.

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

  5. String使用注意说明: 结论: 如果我们对String 做大量修改, 不要使用String(面试官).

    String s = "a";// 创建一个字符串
    s += "b";// 实际上原来的"a"字符串对象已经丢弃了,现在又产生了一个字符串 s+"a" (也即是"ab")。如果多次执行这些改变字符串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放在循环中,会极大影响程序的性能 => 结论:如果我们对String 做大量修改,不要使用String
    

效率测试

  • 效率:StringBuilder > StringBuffer > String.
int loop = 80000;
// StringBuffer
long startTime = 0L;
long endTime = 0L;
StringBuffer stringBuffer = new StringBuffer();
startTime = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
    stringBuffer.append(String.valueOf(i)).append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer=" + (endTime - startTime));// 14ms

// StringBuilder
StringBuilder stringBuilder = new StringBuilder();
startTime = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
    stringBuilder.append(String.valueOf(i)).append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder=" + (endTime - startTime));// 8ms

// String
String text = "";
startTime = System.currentTimeMillis();
for (int i = 0; i < loop; i++) {
    text += i;
}
endTime = System.currentTimeMillis();
System.out.println("String=" + (endTime - startTime));// 5181ms

三种使用原则(结论)

  1. 如果字符串存在大量的修改操作,一般使用StringBuffer 或 StringBuilder.
  2. 如果字符串存在大量的修改操作,并在单线程的情况,使用StringBuilder.
  3. 如果字符串存在大量的修改操作,并在多线程的情况,使用StringBuffer.
  4. 如果我们字符串很少修改,被多个对象引用,使用String,比如配置信息等。

StringBuilder 的方法使用和 StringBuffer一样.

Math方法

基本介绍

  • Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。

Math类常见方法(静态方法)

  • Math类常见方法都是 静态方法 用类名.方法名调用
  • Math类构造器私有化,不能创建对象

ceiling 天花板

floor 地板

// 1.abs 绝对值
int abs = Math.abs(-9);// 9
// 2.pow 求幂
double pow = Math.pow(2, 4);// 2^4 = 16.0
// 3.ceil 向上取整,返回>=该参数的最小整数(转成double)
double ceil = Math.ceil(-3.001);// -3.0
// 4.floor 向下取值,返回<=该参数的最大整数(转成double)
double floor = Math.floor(-4.999);// -5.0
// 5.round 四舍五入 Math.floor(该参数+0.5)
long round = Math.round(-5.555);// -6
// 6.sqrt 求开方 参数不能为负数,否则输出NaN(Not a Number)
double sqrt = Math.sqrt(9.0);// 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
// (int)(a) <= x <= (int)(a + Math.random(b-a+1))

// 结论: 获取一个 a~b 之间的一个随机整数(包含a和b) 
// 取整: [a, a+(b-a+1)) => [a, b+1) => [a, b]
int num = (int)(a + Math.random()*(b-a+1));// 不要忘了"+1". 

// int n = (int)(a); 采取的是小数点后面的数之间去掉的方式,不进行四舍五入的方式处理
// (int)5.1 =>5
// (int)5.9 =>5

// 8.max 求两个数的最大值
int min = Math.min(1, 9);// 1
// 9.min 求两个数的最小值
int max = Math.max(45, 90);// 90

Arrays类

Arrays类常见方法

Negative 负数(负值).

original 起初的.

  • Arrays 里面包含了一系列静态方法,用于管理或操作数组(比如排序和搜索)
// toString: 返回数组的字符串形式,显示数组信息
Arrays.toString(arr);// []
// sort: 排序(自然排序和定制排序)
// binarySearch: 通过二分搜索法进行查找,要求必须排好序
// 1.使用 binarySearch 二叉查找
// 2.要求数组必须有序,如果数组无序,不能使用binarySearch
// 3.如果数组中不存在该元素,就返回 return -(low+1); low:表示元素应该在的索引(从0开始)
int index = Arrays.binarySearch(arr, 3);// 两个参数
// copyOf: 数组元素的复制
// 1.从arr数组中,拷贝 arr.length个元素到 newArr数组中
// 2.如果拷贝长度 > arr.length 就在新数组的后面 增加null,不会抛出异常(可以用来扩充数组)
//   eg. int[] newArr = Arrays.copyOf(oldArr, oldArr.length + 1);
// 3.如果拷贝长度 < 0 就抛出异常NegativeArraySizeException
// 4.该方法的底层使用的是 System.arraycopy()
/*  copyOf()源码
    public static int[] copyOf(int[] original, int newLength) {
        int[] copy = new int[newLength];// 创建一个大小为newLength的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));// 原数据复制
        return copy;// 返回一个copy后的大小为newLength的新数组
    }
*/
Integer[] newArr = Arrays.copyOf(arr, arr.length);
// fill: 数组元素的填充
Integer[] num = new Integer[]{9, 2, 3};
// 解读
// 使用99 去填充 num数组,可以理解成是替换原来的所有元素
Array.fill(num, 99);
// equals: 比较两个数组元素内容是否完全一致
// 1.如果arr 和 arr2 数组的元素完全一样,则方法返回true
// 2.如果不完全一样,就返回false
boolean equals = Array.equals(arr, arr2);
// asList: 将一组值,转换成list
// 1.asList方法,会将(2,3,4,5,6,1)数据转成一个List集合
// 2.返回的 asList 编译类型 List(接口)
// 3.asList 运行类型 java.util.Arrays$ArrayList,是Arrays类的 静态内部类 private static class ArrayList<E> extends AbstractList<E>implements RandomAccess, java.io.Serializable
List<Integer> asList = Arrays.asList(2,3,4,5,6,1);
System.out.println("asList=" + asList);
//sort方法的使用
Integer arr[] = {1, -1, 7, 0, 89};
// 1.可以使用冒泡排序(麻烦且不高效), 也可以直接使用Arrays提供的sort方法排序
// 2.因为数组是引用类型,所以通过sort排序后,会直接影响到 实参arr
Arrays.sort(arr);// 默认排序方法
// 3.sort方法是重载的,也可以通过传入一个接口 Comparator 实现定制排序
// 4.调用 定制排序 时,传入两个参数(1)排序的数组 (2)实现了Comparator接口的匿名内部类,要求实现 compare方法
// 5.这里体现了接口编程的方式,看看源码
//   源码分析
// (1)Arrays.sort(arr, new Comparator()
// (2)最终到 TimSort类的 static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,T[] work, int workBase, int workLen)
// (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) {
//    	    int i1 = (int) o1;
//    	    int i2 = (int) o2;
//      	  return i2 - i1;
//    	}
//	}
// (5)public int compare(Object o1, Object o2)返回的结果直接影响排序的效果
// (*)这里充分体现了 接口编程+动态绑定+匿名内部类的综合使用,将来的底层框架和源码的这种使用方式,会非常常见

Arrays.sort(arr, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        // 1.自定义排序模板
        // 2. o1, o2分别是arr的两个元素,和arr的元素类型一样
        int i1 = (int) o1;
        int i2 = (int) o2;
        // 3.当返回值是小数的时候,利用if判断来实现
        //   因为该方法的返回值类型只能是int
        //   if (i1 - i2 > 0) {
        //	    return 1;
        //   }
        return i2 - i1;
    }
});

System类

System类常见方法

// exit:退出当前程序
// arraycopy: 复制数组元素,比较适合底层调用,一般使用 Array.copyOf完成复制数组
int[] src = {1,2,3};
int[] dest = new int[3];
System.arraycopy(src, 0, dest, 0, s)
// currentTimeMillens: 返回当前时间距离1970-1-1 的毫秒数
// gc:运行垃圾回收机制 System.gc();
// exit:退出当前程序
// 解读: 
// 1.exit(0); 表示程序退出
// 2.参数0 表示一个状态,正常的状态
System.exit(0);
// arraycopy: 复制数组元素,比较适合底层调用,一般使用 Array.copyOf完成复制数组
/*
	
	* @param      src      the source array.源数组
	  srcPos:从源数组的那个索引位置开始拷贝(索引从0开始)
    * @param      srcPos   starting position in the source array.
    * @param      dest     the destination array.目标数组:即把源数组的数据拷贝到的那个数组
    * @param      destPos  starting position in the destination data.目标数组的索引(从0)
    * @param      length   the number of array elements to be copied.  拷贝元素的数量  
*/
int[] src = {1,2,3};
int[] dest = new int[3];
System.arraycopy(src, 0, dest, 0, s)

大数处理方案

BigInteger 和 BigDecimal 常用方法

// add: 加
// subtract: 减
// multiply: 乘
// divide: 除
// BigInteger 常用方法
BigInteger bigInteger = new BigInteger("12345678910");
BigInteger bigInteger2 = new BigInteger("1");
//System.out.println(bigInteger + 1);// 不能使用 + 计算
// 加减乘除需要调用相应方法
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);
// BigDecimal
BigDecimal bigDecimal = new BigDecimal("12.012345678910111213");
BigDecimal bigDecimal2 = new BigDecimal("3");
// System.out.println(bigDecimal + 3);// 不能使用 + 运算
// 使用方法进行 加减乘除 运算
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));// 除

应用场景

  1. BigInteger: 适合保存比较大的整数
  2. BigDecimal: 适合保存精度更高的浮点数(小数)

日期类(Date类)

第一代日期类

  1. Date:精确到毫秒,代表特定的瞬间。
  2. SimpleDateFormat:格式和解析日期的类 SimpleDateFormat 格式化和解析日期的具体类。它允许进行格式化(日期->文本)、解析(文本->日期) 和 规范化。
// 解读
// 1.获取当前系统时间
// 2.这里的Date 类是在java.util 包
// 3.默认输出的日期格式是国外的方式,因此通常需要对格式进行转换
Date date = new Date();
System.out.println(date);
Date date1 = new Date(999999999);// 通过指定毫秒数得到时间
System.out.println(date1);
// 解读
// 1.创建SimpleDateFormat对象,可以指定相应的格式
// 2.这里的格式使用的字母是规定好的,不能乱写
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss E");
String format = sdf.format(date);// format:将日期转换成指定格式的字符串
System.out.println(format);
// 解读
// 1.可以把一个格式化的String 转成对应的 Date
// 2.得到Date 仍然在输出时,还是按照国外的形式,如果希望指定格式输出,需要转换
// 3.在把一个String -> Date,使用sdf 格式需要和你给的 String的格式一样,否则会抛出转换异常
String str = "2022-01-01 01:01:01 星期六";
Date parse = sdf.parse(str);
System.out.println(parse);
System.out.println(sdf.format(parse));

第二代日期类

  1. 第二代日期类,主要就是 Calendar类(日历).
  2. Calendar 类是一个抽象类,它为特定瞬间与一组诸如 YEAR、MONTH、DAY_OF_MONTH、HOUR等 日历字段之间的转换提供了一些方法,并为操作日历字段(例如获取下星期的日期)提供了一些方法。
// 解读
// 1.Calendar是一个抽象类,并且构造器protected
// 2.可以通过 getInstance() 来获取实例
// 3.提供大量的方法和字段提供给程序员
// 4.Calendar没有提供对应的格式化的类,因此需要程序员自己组合来输出(灵活)
// 5.如果我们需要按照 24小时进制来获取时间,Calendar.HOUR ==改为=> Calendar.HOUR_OF_DAY
Calendar instance = Calendar.getInstance();// 创建日历对象
System.out.println(instance);
// 6.获取日历对象的某个日历字段
System.out.println("年:" + instance.get(Calendar.YEAR));
// 这里为什么要 +1,因为Calendar 返回月的时候,是按照 0 开始编号的
System.out.println("月:" + (instance.get(Calendar.MONTH) + 1));
System.out.println("日:" + instance.get(Calendar.DATE));
System.out.println("小时:" + instance.get(Calendar.HOUR));
System.out.println("分钟:" + instance.get(Calendar.MINUTE));
System.out.println("秒:" + instance.get(Calendar.SECOND));
// Calendar 没有专门的格式化方法,使用需要程序员自己来组合显示
System.out.println(instance.get(Calendar.YEAR)
+ "-" + instance.get(Calendar.MONTH)
+ "-" + instance.get(Calendar.DAY_OF_MONTH)
+ " " + instance.get(Calendar.HOUR_OF_DAY)
+ ":" + instance.get(Calendar.MINUTE)
+ ":" + instance.get(Calendar.SECOND));

第三代日期类

前面两代日期类的不足分析

  • JDK1.0中包含了一个 java.util.Date类,但是它的大多数方法已经在 JDK1.1引入 Calendar类之后就弃用了。而Calendar也存在问题是:
    1. 可变性:像日期和时间这样的类应该是不可变的.
    2. 偏移性:Date中的年份是从1900开始的,而月份都从0开始。
    3. 格式化:格式化只对Date有用,Calendar则不行。
    4. 此外:它们也不是线程安全的;不能处理闰秒等(每隔2天,多出1s).

第三代日期类常见方法

  1. LocalDate(日期/年月日)、LocalTime(时间/时分秒)、LocalDateTime(日期时间/年月日时分秒) JDK8加入

    • LocalDate:只包含日期,可以获取日期字段
    • LocalTime:只包含时间,可以获取时间字段
    • LocalDateTime:包含日期+时间,可以获取日期和时间字段
  2. DateTimeFormatter格式日期类 类似于SimpleDateFormat

    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(格式)

    String str = dtf.format(日期对象)

  3. Instant 时间戳

    • 类似于 Date

    • 提供了一系列和Date类转换的方式

    • Instant --> Date

      Date date = Date.from(instant);

    • Date --> Instant

      Instant instant = date.toInstant();

// 第三代日期
// 解读
// 1.使用now() 返回表示当前日期时间的对象
LocalDateTime ldt = LocalDateTime.now();// LocalDate.now();// LocalTime.now();
System.out.println(ldt);
// 2.使用DateTimeFormatter 对象来进行格式化
//   创建 DateTimeFormatter 对象
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = dtf.format(ldt);
System.out.println(format);

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());
LocalDate now = LocalDate.now();

// 提供 plus 和 minus方法可以对当前时间进行加或者减
LocalDateTime plusDays = ldt.plusDays(890);
System.out.println(dtf.format(plusDays));

LocalDateTime minusMinutes = ldt.minusMinutes(3456);
System.out.println(dtf.format(minusMinutes));
// 1.通过静态方法 now() 获取表示当前时间戳的对象
Instant instant = Instant.now();
System.out.println(instant);
// 2.通过 form 可以把 Instant 转成 Date
Date date = Date.from(instant);
// 3.通过 date 的 toInstant() 可以把 date 转成 Instant对象
Instant instant2 = date.toInstant();

集合

集合的理解和好处

数组的不足

  1. 长度开始时必须指定,而且一旦指定,不能更改
  2. 保存的必须为同一类型的元素
  3. 使用数组进行增加/删除元素的示意代码 - 比较麻烦

写出Person数组扩容示例代码:

Person[] pers = new Person[1]; // 数组的大小为1
pers[0] = new Person();

增加新的Person对象:

// 1.创建新的数组
Person[] newPers = new Person[pers.length + 1];
// 2.将旧的数组内容 拷贝到 新的数组
// 2.1利用for循环 来进行数组拷贝,拷贝长度为旧数组的长度
for (int i = 0; i < pers.length; i++) {
    newPers[i] = pers[i];
}
// 2.2利用Arrays.copyOf()方法,返回一个新的数组
newPers = Arrays.copyOf(pers, pers.length + 1);// 这里又重新创建了一个数组
// 3.添加新数据
newPers[newPers.length - 1] = new Person();
// 4.指向新数组
pers = newPers;

集合的优点

  1. 可以动态保存任意多个对象,使用比较方便!
  2. 提供了一系列方便的操作对象的方法: add、remove、set、get等.
  3. 使用集合添加,删除新元素的示例代码 - 简洁了.

集合的框架体系

  • java的集合类很多,主要分为两大类【背下来
  1. 集合主要是两组(单列集合、双列集合)
  2. Collection 接口有两个重要的子接口 List Set ,它们的实现子类都是单列集合
  3. Map 接口的实现子类 是双列集合,存放 K-V (键值对)
  4. 记住下面的两张表

image-20220103143402137.

image-20220103143248288.

Collection接口和常用方法

Collecton 接口实现类的特点

public interface Collection<E> extends Iterable <E>.

  1. Collection 实现子类可以存放多个元素,每个元素可以是 Object (基本数据类型会自动装箱)
  2. 有些Collection的实现类,可以存放重复的元素,有些不可以
  3. 有些Collection的实现类,有些是有序的(List),有些不是有序的(Set)
  4. Collecton接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的

Collection接口常用方法,以实现子类ArrayList来演示

// 1.add:添加单个元素 (这里与put方法区分)
// 2.remove:删除指定元素
// 3.contains:查找元素是否存在
// 4.size:获取元素个数
// 5.isEmpty:判断是否为空
// 6.clear:清空
// 7.addAll:添加多个元素
// 8.containsAll:查找多个元素是否都存在
// 9.removeAll:删除多个元素
List list = new ArrayList();
// 1.add:添加单个元素
// (1)存放的都是对象
// (2)基本数据类型会进行自动装箱处理
list.add("jack");
list.add(10);//list.add(Integer.valueOf(10));// 自动装箱
list.add(true);// list.add(Boolean.valueOf(true));// 自动装箱
// 2.remove:删除指定元素
// (1)根据对象内容删除,返回布尔值
// (2)根据索引下标删除(索引从0开始),返回删除对象,越界会抛出异常
// (3)list.remove(Integer.value(6));// 根据对象删除内容, 这里进行基本数据类型的装箱操作,
//    变成了引用类型(对象),删除的是对象内容
//    list.remove(6);// 根据索引删除元素, 删除的是索引下标为6的元素
// (4)总结: 1.删除元素时,输入一个整数,如果不主动进行装箱操作,就按照索引删除.否则根据对象删除
//         2.其他基本数据类型会自动装箱,不用考虑 (因为这里整数和根据索引删除冲突,所以不在进行自动装箱)
//         3.这里就整数比较特殊
boolean b = list.remove("jack");// 情况(1)
Object o = list.remove(1);// 情况(2)
System.out.println(list);
// 3.contains:查找元素是否存在
System.out.println(list.contains(10));// T
// 4.size:获取元素个数
System.out.println(list.size());
// 5.isEmpty:判断是否为空
System.out.println(list.isEmpty());
// 6.clear:清空
System.out.println(list);
list.clear();
System.out.println(list);
// 7.addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("水浒传");
list.addAll(list2);
System.out.println(list);
// 8.containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));
// 9.removeAll:删除多个元素
list.removeAll(list2);
System.out.println(list);

Collection接口遍历元素方式1-使用Iterator(迭代器)(i)

基本介绍

  1. Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素。
  2. 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器。
  3. Iterator 的结构.[看一张图]
  4. Iterator 仅用于遍历集合,Iterator 本身并不存放对象。

迭代器的执行原理

Iterator iterator = collection.iterator(); // 得到一个集合的迭代器。
// hasNext(): 判断是否还有下一个元素。
while(iterator.hasNext()) {
 	// next():(1)首先指针下移 (2)在将下移以后集合位置上的元素返回
    Object obj = iterator.next();
}
// 当退出 while 循环后,这时 iterator迭代器,指向最后的元素
// iterator.next(); // NoSuchElementException
// 如果希望再次遍历,需要重置我们的迭代器
iterator = collection.iterator();

迭代器示意图

image-20220104162231014.

image-20220104162308142.

image-20220104162345703.

Iterator接口的方法

hasNext()next();

  • 提示:在调用 iterator.next() 方法之前必须要调用 iterator.hasNext() 进行检测。若不调用,且下一条记录无效,直接调用 iterator.next() 会抛出 NoSuchElementException 异常。

示例代码

Collection col = new ArrayList();
col.add(new Book("红楼梦", "曹雪芹", 51.2));
col.add(new Book("小李飞刀", "古龙", 25.1));
col.add(new Book("三国演义", "罗贯中",10.2));

// System.out.println(col.toString());
// 遍历col集合
// 1.先得到 col 对应的 迭代器
Iterator iterator = col.iterator();
// 2.使用while循环遍历
while (iterator.hasNext()) {// 判断是否还有数据
    // 返回下一个元素,类型是 Object
    Object obj = iterator.next();
    System.out.println("obj = " + obj);
}
// 快捷键,快速生成 while => itit
// 显示所有的快捷键的快捷键 ctrl + j
//        while (iterator.hasNext()) {
//            Object obj2 = iterator.next();
//            System.out.println(obj2);
//        }
// 3.当退出 while 循环后,这时iterator迭代器,指向最后的元素
//   iterator.next();// NoSuchElementException
// 4.如果希望再次遍历,需要重置我们的迭代器
iterator = col.iterator();
while (iterator.hasNext()) {
    Object obj3 = iterator.next();
    System.out.println(obj3);
}

Collection接口遍历对象方式2 - for循环增强(iter)

基本介绍

  • 增强 for 循环,可以代替 iterator 迭代器,特点:增强 for 就是简化版的 iterator,本质一样(Debug证明)。只能用于遍历集合或数组。

基本语法

for (元素类型 元素名 : 集合名或数组名) {// 用 : 分隔
    访问元素
}

示例代码

Collection col = new ArrayList();
col.add(new Book("红楼梦", "曹雪芹", 51.2));
col.add(new Book("小李飞刀", "古龙", 25.1));
col.add(new Book("三国演义", "罗贯中", 10.2));
// 解读
// 1.使用增强for,在Collection集合
// 2.增强for,底层仍然是迭代器(Debug)
// 3.增强for可以理解成就是简化版的 迭代器遍历
// 4.快捷键方式 集合名.for
for (Object book : col) {
    System.out.println("book=" + book);
}
// 增强for,也可以直接在数组上使用
//        int[] nums = {1, 2, 6, 5};
//        for (int num : nums) {
//            System.out.println("num=" + num);
//        }

List接口和常用方法

List接口基本介绍

  • List 接口是 Collection 接口的子接口
  1. List 集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  2. List 集合类中的每个元素都有其对应的顺序索引,即支持索引。索引从 0 开始.
  3. List 容器中的每个元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
  4. JDK API 中 List 接口对应的实现类有:
    • image-20220107122254065.
    • 常用的有:ArrayListLinkedListVector.

List接口的常用方法

  • List 集合里添加了一些根据索引来操作集合元素的方法
// 1.void add(int index, Object ele);// 在index位置插入ele元素
// 2.boolean addAll(int index, Collection eles);// 从index位置开始将eles中的所有元素添加进来
// 3.Object get(int index);// 获取指定index位置的元素
// 4.int indexOf(Object obj);// 返回obj在集合中首次出现的位置
// 5.int lastIndexOf(Object obj);// 返回obj在当前集合中末次出现的位置
// 6.Object remove(int index);// 移除指定index位置的元素,并返回此元素
// 7.Object set(int index, Object ele);// 设置指定index位置的元素为ele,相当于替换
// 8.List subList(int fromIndex, int toIndex);// 返回从fromIndex到toIndex位置的子集合
@SuppressWarnings({"all"})
public static void main(String[] args) {
    List list = new ArrayList();
    // 1.1 boolean add(Object ele);// 默认在集合尾部(size)插入元素 elementData[size++] = e;
    list.add("张三丰");
    list.add("贾宝玉");
    // 1.2 void add(int index, Object ele);// 在index位置插入ele元素
    //     index的范围是 [0, size] 0<=index<=size
    list.add(1, "lemon");
    System.out.println(list);// [张三丰, lemon, 贾宝玉]
    // 2.boolean addAll(int index, Collection eles);// 从index位置开始将eles中的所有元素添加进来
    //   index的范围同上 [0, size]
    List list2 = new ArrayList();
    list2.add("jack");
    list2.add("tom");
    list.addAll(3, list2);
    System.out.println(list);// [张三丰, lemon, 贾宝玉, jack, tom]
    // 3.Object get(int index);// 获取指定index位置的元素
    //   index的范围 [0, size) ==> [0, size-1]
    System.out.println(list.get(4));// tom
    list.add("jack");
    System.out.println(list);
    // 4.int indexOf(Object obj);// 返回obj在集合中首次(第一次)出现的位置
    int i = list.indexOf("jack");
    System.out.println(i);// 3
    // 5.int lastIndexOf(Object obj);// 返回obj在当前集合中末次(最后一次)出现的位置
    System.out.println(list.lastIndexOf("jack"));
    // 6.Object remove(int index);// 移除指定index位置的元素,并返回此元素
    Object obj = list.remove(0);
    System.out.println(obj);// 张三丰
    System.out.println(list);// [lemon, 贾宝玉, jack, tom, jack]
    // 7.Object set(int index, Object ele);// 设置指定index位置的元素为ele,相当于替换
    //   (1)返回值是修改之前的原数据
    //   (2)index的范围是[0, size) ==> [0, size-1] 和 get(index) 的范围一致
    Object o = list.set(0, "apple");
    System.out.println(o);
    System.out.println(list);
    // 8.List subList(int fromIndex, int toIndex);// 返回从fromIndex到toIndex位置的子集合
    //   (1)返回的子集合(subList)的范围是 [fromIndex, toIndex) ==> [fromIndex, toIndex]
    //   (2)返回的子集合的长度是 toIndex - fromIndex 和前面字符串截串的方法substring()的索引范围一致
    List subList = list.subList(0, 1);
    System.out.println(subList);
}

List的三种遍历方式[ArrayList,LinkedList,Vector]

  • 说明:使用LinkedList 完成 使用方式和 ArrayList 一样。
// 1.使用iterator
// 快捷键: itit
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    Object obj = iterator.next();
    System.out.println(obj);
}
// 2.使用增强for
// 快捷键: list.for
for (Object o : list) {
    System.out.println(o);
}
// 3.使用普通for
// 快捷键: list.size().for
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

如何对List集合进行排序

public static void bubble(List list) {// 冒泡排序
    int listSize = list.size();
    for (int i = 0; i < listSize - 1; i++) {
        for (int j = 0; j < listSize - 1 - i; j++) {
            // 向下转型
            Book book1 = (Book) list.get(j);
            Book book2 = (Book) list.get(j + 1);
            if (book1.getPrice() > book2.getPrice()) {// 利用set方法,实现交换
                list.set(j, book2);
                list.set(j + 1, book1);
            }
        }
    }
}

ArrayList底层结构和源码分析

ArrayList的注意事项

permits 允许,准许.

synchronized 同步的.

  1. permits all elements,including null,ArrayList 可以加入null,并且可以加入多个.
  2. ArrayList 是由数组来实现数据存储的.
  3. ArrayList 基本等同于Vector,除了ArrayList线程不安全(执行效率高),看源码。在多线程情况下,不建议使用ArrayList.

ArrayList的底层操作机制源码分析

ArrayList扩容机制
  1. ArrayList 中维护了一个Object类型的数组elementData[].

    transient Object[] elementData; // transient 表示瞬间,短暂的,表示该属性不会被序列化.

  2. 当创建ArrayList 对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。

  3. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要再次扩容,则直接扩容elementData为1.5倍。

    建议多追追源码.

ArrayList底层源码-无参构造器

Capacity 容量.

ensure 确保.

Internal 内部.

grow 扩大.

calculate 计算.

Explicit 明确的.

无参构造器示意图

image-20220107122254065.

方法调用顺序-无参构造器

image-20220108181323606.

显示集合完整内容

image-20220108152205958.

ArrayList底层源码-有参构造器

initial 最初的.

image-20220107122254065.

方法调用顺序-有参构造器

image-20220108181323606.

Vector底层结构和源码剖析

Vector基本介绍

  1. Vector类的定义说明:

    image-20220108195007421.

  2. Vector底层也是一个对象数组,protected Object[] elementData;

  3. Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized.

    public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
    
        return elementData(index);
    }
    
  4. 在开发中,需要线程同步安全时,考虑使用Vector.

Vector和ArrayList的比较

底层结构 版本 线程安全(同步),效率 扩容倍数
ArrayList 可变数组 jdk1.2 不安全,效率高 如果使用有参数构造器按照1.5倍扩容 如果使用无参构造器 1.第一次扩容10。2.从第二次开始按1.5倍扩
Vector 可变数组 jdk1.0 安全,效率不高 如果是无参构造器,默认10,满后,就按2倍扩容 如果指定大小(有参构造器),则每次直接按2倍扩容

Vector源码解读

public static void main(String[] args) {
    // 无参构造器
    Vector vector = new Vector();
    for (int i = 0; i < 10; i++) {
        vector.add(i);
    }
    vector.add(100);
    /*
        1.new Vector()的底层
            // (1)调用无参构造器
            public Vector() {
                this(10);// 调用有参构造器
            }
            // (2)调用一个参数的构造器
            public Vector(int initialCapacity) {
                this(initialCapacity, 0);// 调用另一个有参构造器
            }
            // (3)调用两个参数的构造器
            public Vector(int initialCapacity, int capacityIncrement) {
                super();// 调用父类的无参构造器 protected AbstractList() {}
                if (initialCapacity < 0)
                    throw new IllegalArgumentException("Illegal Capacity: "+
                                                       initialCapacity);
                // 创建大小为initialCapacity的 Object[], 并赋给elementData
                this.elementData = new Object[initialCapacity];
                // 设置容量增量,默认为0
                // Increment: 增量
                this.capacityIncrement = capacityIncrement;
            }
        2.vector.add(i);
            // (1)添加数据到vector集合中
            public synchronized boolean add(E e) {
                modCount++;
                ensureCapacityHelper(elementCount + 1);
                elementData[elementCount++] = e;
                return true;
            }
            // (2)确定是否需要扩容 条件:minCapacity - elementData.length > 0
            // minCapacity: 需要的最小空间大小
            // elementData.length: 实际数组的空间大小
            // 如果minCapacity > elementData.length: 说明需要的最小空间大小大于实际数组的空间大小,即不能由实际数组的空间容量提供, 因此需要扩容
            //    minCapacity <= elementData.length: 说明实际的数组空间够用, 不需要扩容
            private void ensureCapacityHelper(int minCapacity) {
                // overflow-conscious code
                if (minCapacity - elementData.length > 0)
                    grow(minCapacity);
            }
            // (3)grow()扩容源码
            // 关键: int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
            //                                       capacityIncrement : oldCapacity);
            // 三目运算符: ((capacityIncrement > 0) ? capacityIncrement : oldCapacity)
            // 如果capacityIncrement(容量增量, 默认为0) > 0为true(默认为false), 就返回 capacityIncrement.
            // 否则返回 oldCapacity(这里相当于2倍扩容).
            // 即默认扩容2倍
            private void grow(int minCapacity) {
                // overflow-conscious code
                int oldCapacity = elementData.length;
                int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                                 capacityIncrement : oldCapacity);
                if (newCapacity - minCapacity < 0)
                    newCapacity = minCapacity;
                if (newCapacity - MAX_ARRAY_SIZE > 0)
                    newCapacity = hugeCapacity(minCapacity);
                elementData = Arrays.copyOf(elementData, newCapacity);
            }
         */
}

LinkedList底层结构

LinkedList的全面说明

  1. LinkedList 底层实现了双向链表双端队列特点。
  2. 可以添加任意元素(元素可以重复),包括null.
  3. 线程不安全,没有实现同步.

LinkedList的遍历方式(3种)

public static void main(String[] args) {
    // LinkedList的三种遍历方式
    LinkedList linkedList = new LinkedList();
    linkedList.add("jack1");
    linkedList.add("smith2");
    linkedList.add("marry3");
    // (1)普通for循环
    System.out.println("===LinkedList 普通for循环===");
    for (int i = 0; i < linkedList.size(); i++) {
        System.out.println(linkedList.get(i));
    }
    // (2)增强for循环
    System.out.println("===LinkedList 增强for循环===");
    for (Object o : linkedList) {
        System.out.println(o);
    }
    // (3)迭代器
    System.out.println("===LinkedList 迭代器===");
    Iterator iterator = linkedList.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        System.out.println(obj);
    }
}

LinkedList的底层操作机制

  1. LinkedList 底层维护了一个双向链表。
  2. LinkedList中维护了两个属性firstlast分别指向 首结点和尾结点。
  3. 每个结点(Node对象),里面又维护了prevnextitem三个属性,其中通过prev指向前一个,通过next指向后一个结点。最终实现双向链表。
  4. 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率更高

LinkedList源码图解

  • LinkedList添加元素源码阅读
// LinkedList添加元素源码阅读:
public static void main(String[] args) {
    LinkedList linkedList = new LinkedList();
    linkedList.add(1);
    linkedList.add(100);
    System.out.println(linkedList);
    /*	LinkedList添加元素源码阅读:
            1.LinkedList linkedList = new LinkedList();
                public LinkedList() {}
            2.这时,linkedList 的属性 first=null  last=null
            3.执行添加元素 linkedList.add(1);
                public boolean add(E e) {
                    linkLast(e);
                    return true;
                }
            4.关键: 执行 linkLast(e);// 将新的结点,加入到双向链表的最后
                void linkLast(E e) {
                    final Node<E> l = last;
                    final Node<E> newNode = new Node<>(l, e, null);
                    last = newNode;
                    if (l == null)
                        first = newNode;
                    else
                        l.next = newNode;
                    size++;
                    modCount++;
                }
         */
}

image-20220109180144112.

image-20220109182952371.

  • LinkedList删除元素源码阅读
public static void main(String[] args) {
    LinkedList linkedList = new LinkedList();
    linkedList.add(1);
    linkedList.add(100);
    linkedList.add(300);
    System.out.println(linkedList);
    linkedList.remove();
    System.out.println(linkedList);
    /*    LinkedList删除元素源码阅读:
            1.执行删除元素 linkedList.remove();
                public E remove() {
                    return removeFirst();// 默认删除LinkedList的第一个元素
                }
            2.执行 removeFirst(); // 删除第一个元素
                public E removeFirst() {
                    final Node<E> f = first;
                    if (f == null)
                        throw new NoSuchElementException();
                    return unlinkFirst(f);
                }
            3.关键: 执行 unlinkFirst(Node<E> f) 真正删除第一个元素
                private E unlinkFirst(Node<E> f) {
                    // assert f == first && f != null;
                    final E element = f.item;// 保存删除元素的信息
                    final Node<E> next = f.next;
                    f.item = null;
                    f.next = null; // help GC
                    first = next;
                    if (next == null)
                        last = null;
                    else
                        next.prev = null;
                    size--;
                    modCount++;
                    return element;
                }
         */
}

image-20220109184752564.

  • LinkedList查找元素源码阅读
public static void main(String[] args) {
    LinkedList linkedList = new LinkedList();
    linkedList.add(1);
    linkedList.add(100);
    linkedList.add(300);
    System.out.println(linkedList);
    linkedList.remove();
    System.out.println(linkedList);
    Object obj = linkedList.get(1);
    System.out.println(obj);
    // LinkedList查找元素源码阅读:
    /*
            1.执行查找元素 Object obj = linkedList.get(1);
                public E get(int index) {
                    checkElementIndex(index);// 检查索引是否正常
                    return node(index).item;
                }
            2.检查元素索引是否正常 checkElementIndex(int index)
                private void checkElementIndex(int index) {
                    // 如果索引正常,返回true,取反 false
                    // 反之,索引异常,返回false,取反 true,抛出异常
                    if (!isElementIndex(index))
                        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
                }
            3.真正检查索引是否正常
                private boolean isElementIndex(int index) {
                    // 索引范围 [0, size) ==> [0, size-1]
                    return index >= 0 && index < size;
                }
            4.真正的查找结点 Node<E> node(int index)
                Node<E> node(int index) {
                    // assert isElementIndex(index);
                    // (size >> 1) 等价于 (size / 2), 但前者效率高
                    if (index < (size >> 1)) {
                        Node<E> x = first;
                        // for循环查找结点
                        for (int i = 0; i < index; i++)
                            x = x.next;
                        return x;
                    } else {
                        Node<E> x = last;
                        for (int i = size - 1; i > index; i--)
                            x = x.prev;
                        return x;
                    }
                }
         */

}
  • LinkedList修改元素源码阅读
public static void main(String[] args) {
    LinkedList linkedList = new LinkedList();
    linkedList.add(1);
    linkedList.add(100);
    linkedList.add(300);
    System.out.println(linkedList);
    linkedList.remove();
    System.out.println(linkedList);
    linkedList.set(1, 200);
    System.out.println(linkedList);
    // LinkedList修改元素源码阅读:
    /*
            1.执行修改元素 linkedList.set(1, 200);
                public E set(int index, E element) {
                    checkElementIndex(index);
                    Node<E> x = node(index);
                    E oldVal = x.item;
                    x.item = element;
                    return oldVal;
                }
            2.checkElementIndex(index);
              Node<E> x = node(index);
              这两步和LinkedList查找元素相同
            3.修改值的关键
                E oldVal = x.item;// 保存旧值
                x.item = element;// 修改值
                return oldVal;// 返回旧值
         */
}

ArrayList和LinkedList比较

  • ArrayList和LinkedList的比较

    底层结构 增删的效率 改查的效率 线程安全
    ArrayList 可变数组 较低,数组扩容 较高, 通过索引直接定位 不安全
    LinkedList 双向链表 较高,通过链表追加 较低 不安全
  • 如果选择ArrayList和LinkedList:

    1. 如果我们改查的操作多,选择ArrayList.
    2. 如果我们增删的操作多,选择LinkedList.
    3. 一般来说,在程序中,80%~90%都是查询,因此大部分情况下会选择ArrayList.
    4. 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择.

Set接口和常用方法

Set接口基本介绍

  1. 无序(添加和取出的顺序不一致),没有索引.

  2. 不允许重复元素,所以最多包含一个null.

  3. JDK API中Set接口的实现类有:

    image-20220109201449629.

Set接口的常用方法

  • 和List接口一样,Set接口也是Collection的子接口,因此常用方法和Collection接口一样。
  • Collection接口常用方法.

Set接口的遍历方式

  • 同Colleciton的遍历方式一样,因为Set接口是Collection接口的子接口。
    1. 可以使用迭代器.
    2. 增强for.
    3. 不能使用索引的方式来获取。

Set集合示例代码

public static void main(String[] args) {
    // 解读:
    // 1.以Set接口的实现类 HashSet来讲解 Set接口的方法
    // 2.Set 接口的实现类的对象(Set接口对象), 不能存放重复的元素, 可以添加一个null
    // 3.Set 接口对象存放数据是无序的(即添加的顺序和取出的顺序不一致)
    // 4.注意: 取出的顺序虽然不是添加的顺序, 但是它是固定的.
    Set set = new HashSet();
    set.add("jack");
    set.add("john");
    set.add("jack");
    set.add(null);
    set.add(null);
    set.add("tom");
    for (int i = 0; i < 10; i++) {
        System.out.println(set);
    }
    // 遍历(2种方式)
    // 方式一: 使用迭代器
    System.out.println("===Set遍历迭代器===");
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        System.out.println(obj);
    }
    // 方式二: 增强for (底层还是迭代器)
    System.out.println("===Set集合遍历增强for===");
    for (Object o : set) {
        System.out.println(o);
    }
    // 不能使用普通for来遍历
}

Set接口实现类-HashSet

HashSet的全面说明

  1. HashSet实现了Set接口.

  2. HashSet实际上是HashMap,看源码.

    image-20220110093808019.

  3. 可以存放null值,但是只能有一个null.(即元素不能重复)

  4. HashSet不保证元素是有序的,取决于hash后,再确定索引的结果.(即不保证存放元素的顺序和取出顺序一致)

  5. 不能有重复元素/对象.

示例代码

public static void main(String[] args) {
    HashSet hashSet = new HashSet();
    // 说明
    // 1.在执行add方法后,会返回一个boolean值
    // 2.如果添加成功,返回true,否则返回false
    // 3.可以通过remove 指定删除哪个对象.
    System.out.println(hashSet.add("john"));// T
    System.out.println(hashSet.add("lucy"));// T
    System.out.println(hashSet.add("john"));// F
    System.out.println(hashSet.add("jack"));// T
    System.out.println(hashSet.add("Rose"));// T

    hashSet.remove("john");
    System.out.println(hashSet);// 3个
}

问题引入-经典面试题

public static void main(String[] args) {
    HashSet hashSet = new HashSet();
    System.out.println(hashSet);// []
    // 4.HashSet 不能添加相同的元素/数据?
    hashSet.add("lucy");// 添加成功
    hashSet.add("lucy");// 加入不了
    hashSet.add(new Dog("tom"));// OK
    hashSet.add(new Dog("tom"));// OK
    System.out.println(hashSet);

    // 再加深一下,非常经典的面试题
    // 看源码,做分析.
    // 去看源码,即 add 到底发生了什么? => 底层机制
    hashSet.add(new String("hsp"));// OK
    hashSet.add(new String("hsp"));// 加入不了
    System.out.println(hashSet);
}

HashSet底层机制说明

一、数组链表模拟

Structure 结构.

  • 分析 HashSet 底层是 HashMapHashMap 底层是(数组+链表+红黑树).

  • 模拟简单的数组+链表结构

    public class HashSetStructure {
        public static void main(String[] args) {
            // 模拟一个HashSet的底层(HashMap 的底层结构)
    
            // 1.创建一个数组,数组的类型是 Node[]
            // 2.有些人,直接把 Node[] 数组称为 表
            Node[] table = new Node[16];
            // 3.创建结点
            Node john = new Node("john");
            table[2] = john;// 把John 放在 table表的索引为2的位置
            Node jack = new Node("jack");
            john.next = jack;// 将Jack 结点挂载到John
            Node rose = new Node("Rose");
            jack.next = rose;// 将Rose 结点挂载到Jack
            System.out.println(table);
            
            Node lucy = new Node("lucy");
            table[3] = lucy;// 将Lucy 放在 table表的索引为3的位置
        }
    }
    
    class Node {// 结点,存储数据,可以指向下一个结点,从而形成链表
        Object item;// 存储数据
        Node next;// 指向下一个结点
    
        public Node(Object item) {
            this.item = item;
        }
    }
    
  • 示意图:

    image-20220112133806585.

  • 结论:

    1. 先获取元素的哈希值(hashCode方法).

    2. 对哈希值进行运算,得出一个索引值即为要存放的哈希表中的位置号.

    3. 如果该位置上没有其他元素,则直接存放

    4. 如果该位置上已经有其他元素,则需要进行equals判断,如果相等,则不再添加。如果不相等,则以链表的方式添加。

      注意:这个equals() 由程序员确定,不能够简单的理解成就一定是判断内容是否相等.
      根据实际情况,来确定!!!

二、HashSet扩容机制

threshold 阈值,临界值.

  • 分析HashSet添加元素底层是如何实现的(hash() + equals())

  • 结论

    1. HashSet 底层是 HashMap.
    2. 添加一个元素时,先得到 hash 值 - 会转成 -> 索引值.
    3. 找到存储数据表 table,看这个索引位置是否已经存放着元素.
    4. 如果没有,直接加入.
    5. 如果有,调用 equals 比较,如果相同,就放弃添加,如果不同,则添加到最后.
    6. Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(树型阈值 默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(最小树容量 默认64),就会进行树化(红黑树).

三、HashSet转成红黑树机制

  • 结论

    1. HashSet底层是HashMap,第一次添加时,table 数组扩容到 16,临界值(threshold)是 16*加载因子(loadFactor)是0.75=12

      12含义

    2. 如果table 数组使用到了临界值 12,就会扩容到 16*2=32,新的临界值就是32*0.75=24, 依此类推

      16(12) --> 32(24) --> 64(48) --> 128(96)......

    3. Java8中,如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

  • 注意:

    1. 如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),但是此时 table 的大小没有到达MIN_TREEIFY_CAPACITY(默认64),则进行数组扩容机制,不会进行树化操作。

      public class HashSetIncrement {
          public static void main(String[] args) {
              HashSet set = new HashSet();
              for (int i = 1; i <= 8; i++) {
                  set.add(new A(i));
              }
              // 每往元素数为8的链表添加结点,就会导致数组扩容
              // 直到table表的大小为64,然后进行树化
              for (int i = 9; i <= 12; i++) {
                  set.add(new A(i));
              }
          }
      }
      class A {
          private int n;
          public A(int n) {
              this.n = n;
          }
          @Override
          public int hashCode() {
              return 100;// 重写hashCode(), 返回相同的值
          }
      }
      
    2. 如果table 的大小已经到达MIN_TREEIFY_CAPACITY(默认64),但是此时table表中没有任何一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),则还是进行数组扩容机制,不会进行树化操作。

      public static void main(String[] args) {
          HashSet set = new HashSet();
          for (int i = 1; i <= 12; i++) {
              set.add(i);
          }
          // 到达临界值时会进行数组扩容
          for (int i = 13; i <= 24; i++) {
              set.add(i);
          }
          for (int i = 25; i <= 48; i++) {
              set.add(i);
          }
          for (int i = 49; i <= 96; i++) {
              set.add(i);
          }
          set.add(97);
      }
      
    3. 树化操作两个条件缺一不可

四、HashSet源码解读

public static void main(String[] args) {

    HashSet hashSet = new HashSet();
    hashSet.add("java");// 第1次add() 分析完毕
    hashSet.add("php");//
    hashSet.add("java");
    System.out.println("hashSet=" + hashSet);
/*    
    对HashSet 的源码解读
        1.执行 HashSet()
        public HashSet() {
        map = new HashMap<>();
    }
    2.执行 add()
        public boolean add(E e) {// e="java
        return map.put(e, PRESENT)==null;// (static) PRESENT = new Object();仅占位作用
    }
    3.执行 put()
        public V put(K key, V value) {// key="java", value=PRESENT 静态共享
        return putVal(hash(key), key, value, false, true);
    }
    4.执行 hash(key);// 该方法得到key对应的hash值, 该值并不完全等价于 hashCode()值
    注意: hash值不是hashCode值!!!算法=(h = key.hashCode()) ^ (h >>> 16) 无符号右移16位
        static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 尽量避免碰撞
    }
    5.执行 putVal()
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;// 定义了辅助变量
        // table 就是 HashMap 的一个属性, 类型是 Node[]
        // if 语句表示如果当前table 是null, 或者 大小=0
        // 就是第一次扩容, 扩容到16个空间.
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // (1)根据key, 得到hash 去计算该key应该存放到table表的哪个索引位置
        // 并把这个位置的对象, 赋给 p
        // (2)判断p 是否为null
        // (2.1)如果p 为null, 表示这个位置还没有存放元素, 就创建一个Node(key="java", value=PRESENT)
        // (2.2)就放在该位置 tab[i] = newNode(hash, key, value, null);
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            // 一个开发技巧提示: 在需要局部变量(辅助变量)的时候, 再创建
            Node<K,V> e; K k;// 辅助变量
            // 如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
            // 并且满足 下面两个条件之一:
            // (1) p 指向的Node结点的 key 和 准备加入的key 是同一个对象
            // (2) 准备加入的key 的equals() 和 p 指向的Node结点的 key比较后相同
            // 注意:这个equals() 由程序员确定,不能够简单的理解成就一定是判断内容是否相等
            //      根据实际情况,来确定!!!
            // 就不能加入
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 再判断 p 是不是一颗红黑树
            // 如果是一颗红黑树,就调用 putTreeVal(),来进行添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {// 如果table对应索引位置,已经是一个链表,就使用for循环比较
                // (1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后
                // 注意:在把元素添加到链表后,立即判断,该链表是否已经达到8个结点
                // , 如果达到, 就调用 treeifyBin(), 对当前这个链表进行树化(转成红黑树)
                // 注意:在转成红黑树时,还进行判断,判断条件如下
                // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                //            resize();
                // 如果上面条件成立,先将 table表扩容
                // 只有当上面条件不成立时,才进行转成红黑树
                // (2)依次和该链表的每一个元素比较过程中,如果有相同的情况,就直接break
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD(8) - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // size 就是我们每加入一个结点Node(k, v, h, next), size+
        // 我们不管这个结点是加在table表的某个位置,还是加在table表的某条链表上,都算加上一个结点
        // 即size++
        // 不能理解成必须加在table表的某个位置,才是size++,而加在链表上不算size++
        if (++size > threshold)
            resize();// 扩容
        afterNodeInsertion(evict);// HashMap留给子类LinkedHashMap的方法, 本身为空方法
        return null;
    }
*/
}

Set接口实现类-LinkedHashSet

LinkedHashSet的全面说明

  1. LinkedHashSetHashSet 的子类.
  2. LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组+双向链表.
  3. LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的(即是有序的).
  4. LinkedHashSet 不允许添加重复元素.

流程说明

  1. LinkedHashSet 中维护了一个hash表双向链表(LinkedHashSetheadtail)

  2. 每一个结点有 beforeafter 属性,这样可以形成双向链表

  3. 在添加一个元素时,先求hash值,再求索引,确定该元素在table表的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加)【原则和HashSet一样】

    tail.next = newElement;// 示例代码
    newElement.prev = tail;
    tail = newElement;
    
  4. 这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

示例代码

public static void main(String[] args) {
    Set set = new LinkedHashSet();
    set.add(new String("AA"));
    set.add(456);
    set.add(456);
    set.add(new Customer("刘", 1001));
    set.add(123);
    set.add("HSP");

    System.out.println("set=" + set);
    // 解读
    // 1.LinkedHashSet 加入顺序和取出元素/数据的顺序一致
    // 2.LinkedHashSet 底层维护的是一个 LinkedHashMap(是HashMap的子类)
    // 3.LinkedHashSet 底层结构 (数组table+双向链表)
    // 4.添加第一次时,直接将 数组table 扩容到 16, 存放的结点类型是 LinkedHashMap$Entry
    // 5.数组是 HashMap$Node[] 存放的元素/数据是 LinkedHashMap$Entry 类型
    /*
            // 继承关系是在内部类完成的
            static class Entry<K,V> extends HashMap.Node<K,V> {
                Entry<K,V> before, after;
                Entry(int hash, K key, V value, Node<K,V> next) {
                    super(hash, key, value, next);
                }
            }

         */
}

示意图

image-20220114203000164.

Map接口和常用方法

Map接口实现类的特点【很实用】

  • 注意:这里讲的是 JDK8Map 接口特点
  1. MapCollection 并列存在(即 相互之间没有什么关系)。Map用于保存具有映射关系的数据:Key-Value.(双列元素)

  2. Map 中的 keyvalue 可以是任何引用类型的数据,会封装到 HashMap$Node对象中

  3. Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码.

    当有相同的key添加时,就等价于新的值替换旧的value,此时value为新的值.

  4. Map 中的 value 可以重复.

  5. Mapkey 可以为 nullvalue 也可以为 null,注意 keynull只能有一个valuenull,可以多个.

  6. 常用String类作为Mapkey.(但是不仅仅String类型,而是任何引用类型的数据,这里只是说常用String类)

  7. key 和 value 之间存在单向一对一关系,即通过指定的key 总能找到对应的 value.

    // 通过put 方法,传入 key-value
    hashMap.put(key, value)
    // 通过get 方法,传入key,会返回对应的value
    Object obj = hashMap.get(key);
    
  8. Map存放数据的key-value示意图,一对k-v 是放在一个HashMap$Node中的,又因为Node实现了 Entry 接口,有些书上也说 一对k-v 就是一个 Entry.

  9. Map存放数据的key-value示意图:

    image-20220112154336033.

    • 注意:
      1. EntrySet中的Key-Value是简单指向,并非真实数据.
      2. EntrySet 不包括 KeySet 和 Values

示例代码

public static void main(String[] args) {
    Map map = new HashMap();
    map.put("No1", "smith");
    map.put("No2", "marry");

    // 解读
    // 1.k-v 最后是 HashMap$Node node = newNode(hash, key, value, null);
    // 2.k-v 为了方便程序员的遍历,还会 创建 EntrySet 集合,该集合存放的元素的类型 Entry,
    //   而一个Entry 对象就包含有k,v EntrySet<Entry<K,V>> 即: transient Set<Map.Entry<K,V>> entrySet;
    // 3.在entrySet 中,定义的类型是 Map.Entry,但是实际上存放的还是 HashMap$Node
    //   这是因为 static class Node<K,V> implements Map.Entry<K,V>
    // 4.当把 HashMap$Node 对象 存放到 entrySet, 就方便我们的遍历, 因为 Map.Entry 提供了重要方法
    //   K getKey(); V getValue();
    

    Set entrySet = map.entrySet();
    System.out.println(entrySet.getClass());// HashMap$EntrySet(class java.util.HashMap$EntrySet)
    for (Object obj : entrySet) {
        System.out.println(obj.getClass());// HashMap$Node(class java.util.HashMap$Node)
        // 为了从 HashMap$Node 取出k-v
        // 1.先做一个向下转型
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getKey() + "-" + entry.getValue());
    }
    
    Set keySet = map.keySet();
    System.out.println(keySet.getClass());// HashMap$KeySet(class java.util.HashMap$KeySet)
    Collection values = map.values();
    System.out.println(values.getClass());// HashMap$Values(class java.util.HashMap$Values)
}

Map接口常用方法

// 1.put:添加
// 2.remove:根据键删除映射关系
// 3.get:根据键获取值
// 4.size:获取元素个数
// 5.isEmpty:判断个数是否为0
// 6.clear:清除k-v, 清空(慎用)
// 7.containsKey:查找键是否存在

Map接口遍历方法(3组6种)

public static void main(String[] args) {
    Map map = new HashMap();
    map.put("jack", 12);
    map.put("smith", 15);
    map.put("marry", 20);
//-------------------------------第一组-----------------------------------------
    // 第一组:先取出 所有的Key, 通过Key 取出对应的Value
    Set keySet = map.keySet();
    // 方式一:增强for
    System.out.println("===keySet 增强for===");
    for (Object key : keySet) {
        System.out.println(key + "-" + map.get(key));
    }
    // 方式二:迭代器
    System.out.println("===keySet 迭代器===");
    Iterator iterator1 = keySet.iterator();
    while (iterator1.hasNext()) {
        Object key = iterator1.next();
        System.out.println(key + "-" + map.get(key));
    }
//-------------------------------第二组------------------------------------------
    // 第二组:把所有的values取出
    Collection values = map.values();
    // 这里可以使用所有的Collection使用的遍历方法
    // 方式一:增强for
    System.out.println("===values 增强for===");
    for (Object value : values) {
        System.out.println(value);
    }
    // 方式二:迭代器
    System.out.println("===values 迭代器===");
    Iterator iterator2 = values.iterator();
    while (iterator2.hasNext()) {
        Object value = iterator2.next();
        System.out.println(value);
    }
    // 不能使用普通for, 因为 Collection接口没有提供 get()方法
    // 而 List接口提供了 get()方法
//-------------------------------第三组-------------------------------------------
    // 第三组:通过EntrySet 来获取K-V
    Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>
    // 方式一:增强for
    System.out.println("===EntrySet 增强for===");
    for (Object entry : entrySet) {
        // 将entry 转成 Map.Entry
        Map.Entry e = (Map.Entry) entry;
        System.out.println(e.getKey() + "-" + e.getValue());
    }
    // 方式二:迭代器
    System.out.println("===EntrySet 迭代器===");
    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 e = (Map.Entry) entry;
        System.out.println(e.getKey() + "-" + e.getValue());
    }
}

Map接口实现类-HashMap

HashMap小结

  1. Map接口的常用实现类:HashMapHashTableProperties.
  2. HashMapMap接口使用频率最高的实现类.
  3. HashMap 是以 Key-Value (键值对) 的方式来存储数据(HashMap$Node类型).
  4. Key 不能重复,但是Value可以重复,允许使用null键和null值.
  5. 如果添加相同的Key,则会覆盖原来的Key-Value,等同于修改.(Key不会替换,Value会替换)
  6. HashSet 一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.(JDK8HashMap 底层结构 数组 + 链表 + 红黑树)
  7. HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized.

HashMap底层结构和源码剖析

HashMap底层机制

  1. HashMap底层维护了Node类型的数组table,默认为null
  2. 当创建对象时,将加载因子(loadFactor)初始化为0.75
  3. 当添加Key-Value时,通过Key的哈希值得到在table的索引。然后判断该索引处是否有元素,如果没有元素就直接添加。如果该索引处有元素,继续判断该元素的Key和准备加入的Key是否相等,如果相等,则直接替换Value;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
  4. 第1次添加,则需要扩容table容量为16,临界值(threshold)为12 (16*0.75)
  5. 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来临界值的2倍,即24,依此类推.
  6. Java8 中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认是 8),并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树)

HashMap示意图

  1. (K, V) 是一个Node 实现了Map.Entry<K, V>.
  2. jdk7.0的HashMap 底层实现【数组+链表】,jdk8.0 底层 【数组+链表+红黑树】

image-20220113133124762.

HashMap源码解读

public static void main(String[] args) {
    HashMap hashMap = new HashMap();
    hashMap.put("java", 10);// OK
    hashMap.put("php", 20);// Ok
    hashMap.put("java", 30);// 替换value

    System.out.println("hashMap=" + hashMap);

    // 解读HashMap的源码+图解
    // 1.执行构造器 new HashMap();
    //   初始化加载因子 loadFactor = 0.75
    //   HashMap$Node[] table = null
    // 2.执行put()
    //      public V put(K key, V value) {// key="java" value=10
    //          return putVal(hash(key), key, value, false, true);
    //      }
    // 3.调用hash()方法, 计算 key的 hash值. 关键: (h = key.hashCode()) ^ (h >>> 16)
    //      static final int hash(Object key) {
    //          int h;
    //          return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    //      }
    // 4.执行putVal()
    //      final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    //                   boolean evict) {
    //          Node<K,V>[] tab; Node<K,V> p; int n, i;// 辅助变量
    //          // 如果底层的table 数组为null, 或者 length=0, table数组就扩容到16
    //          if ((tab = table) == null || (n = tab.length) == 0)
    //              n = (tab = resize()).length;
    //          // 取出hash值对应的table表的索引位置的Node,如果为null,就直接把加入的K-V
    //          // , 创建成一个Node, 加入该位置即可.
    //          if ((p = tab[i = (n - 1) & hash]) == null)
    //              tab[i] = newNode(hash, key, value, null);
    //          else {
    //              Node<K,V> e; K k;// 辅助变量
    //              // 如果table表的索引位置的key的hash值和新的key的hash值相同,
    //              // 并且满足 (table表中现有的结点的key和准备添加的key是同一个对象 || equals返回真)
    //              // 就认为已经存在该结点,不能够再加入新的K-V
    //              if (p.hash == hash &&
    //                  ((k = p.key) == key || (key != null && key.equals(k))))
    //                  e = p;
    //              // 如果当前的table的已有Node 是红黑树,就按照红黑树的方式处理
    //              else if (p instanceof TreeNode)
    //                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    //              else {
    //                  // 如果找到的结点,后面是链表,就循环比较
    //                  for (int binCount = 0; ; ++binCount) {// 死循环
    //                      // 如果整个链表,没有和他相同的,就加到该链表的最后
    //                      if ((e = p.next) == null) {
    //                          p.next = newNode(hash, key, value, null);
    //                          // 加入后,立即判断当前链表的个数,是否已经到达8个,到达8个后,
    //                          // 就调用 treeifyBin 方法进行红黑树的转换
    //                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    //                              treeifyBin(tab, hash);
    //                          break;
    //                      }
    //                      // 如果在循环比较过程中,发现有相同的,就break,就只是替换Value
    //                      if (e.hash == hash &&
    //                          ((k = e.key) == key || (key != null && key.equals(k))))
    //                          break;
    //                      p = e;
    //                   }
    //              }
    //              if (e != null) { // existing mapping for key
    //                  V oldValue = e.value;
    //                  if (!onlyIfAbsent || oldValue == null)
    //                      e.value = value;// 替换, Key对应的Value
    //                  afterNodeAccess(e);
    //                  return oldValue;
    //                }
    //          }
    //          // 每增加一个Node, 就modCount++
    //          ++modCount;
    //          // 1.++size > threshold:先size++,然后在与threshold比较
    //          // 2.如果size > 临界值[12-24-48...],就扩容
    //          if (++size > threshold)
    //              resize();
    //          afterNodeInsertion(evict);
    //          return null;
    //      }
    // 5.关于树化(转成红黑树)
    // (1)如果table表为null, 或者大小还没有到 64,就暂时不树化,而是进行扩容
    // (2)否则才会真正的树化 -> 剪枝(不断删除元素导致 红黑树 -> 链表,这就叫剪枝)
    //      final void treeifyBin(Node<K,V>[] tab, int hash){
    //          int n, index;
    //          HashMap.Node<K, V> e;
    //          if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
    //              resize();// 扩容
    //
    //      }

}

Map接口实现类-Hashtable

Hashtable的基本介绍

Dictionary 字典.

  1. 存放的元素是键值对:即K-V
  2. Hashtable的键和值都不能null,否则会抛出空指针异常 NullPointerException.
  3. Hashtable 使用方法基本上和HashMap一样.
  4. Hashtable 是线程安全的(synchronized),HashMap 是线程不安全的.
// 示例代码
public static void main(String[] args) {
    Hashtable hashtable = new Hashtable();
    hashtable.put("abc", 100);
    // hashtable.put(null, 100);// NullPointerException
    // hashtable.put("john", null);// NullPointerException
    hashtable.put("bcd", 100);
    hashtable.put("def", 100);
    hashtable.put("def", 88);// 替换

    System.out.println("hashtable=" + hashtable);
    // Hashtable解读
    // 1.Hashtable 底层有数组 Hashtable$Entry[], 初始化大小为 11
    // 2.临界值:threshold = 8 = 11*0.75
    // 3.扩容:按照自己的扩容机制来进行扩容
    // 4.执行 方法 addEntry(hash, key, value, index); 添加K-V 封装到Entry
    // 5.当 if (count >= threshold) 满足时,就进行扩容
    //   按照 int newCapacity = (oldCapacity << 1) + 1;(即: 2倍+1) 的大小扩容
}

Hashtable 和 HashMap对比

版本 线程安全(同步) 效率 允许null键null值
HashMap 1.2 不安全 可以
Hashtable 1.0 安全 较低 不可以

Map接口实现类-Properties

基本介绍

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

  2. 它的使用特点和Hashtable类似

  3. Properties 还可以用于 从xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改.

  4. 说明:工作后 xxx.properties 文件通常作为配置文件.

public static void main(String[] args) {
    // Properties解读
    // 1.Properties 继承 Hashtable
    // 2.可以通过 K-V 存放数据,当然Key和Value 都不能为null
    // 3.增加 删除 查找 修改
    Properties properties = new Properties();
    properties.put("abc", 100);
    //properties.put(null, 100);
    //properties.put("john", null);
    properties.put("bcd", 100);
    properties.put("cde", 100);
    properties.put("cde", 88);
    System.out.println("properties=" + properties);
    // (1)增加
    properties.put("def", 99);
    properties.put("efg", 66);
    System.out.println("properties=" + properties);
    // (2)删除
    Object obj = properties.remove("abc");
    System.out.println(obj);// 100
    // (3)查找 通过K 获取对应值
    System.out.println(properties.get("cde"));// 88
    System.out.println(properties.getProperty("cde"));// null
    // (4)修改
    properties.put("cde", 999);
    System.out.println("properties=" + properties);
}

总结-开发中如何选择集合实现类(记住)

  • 集合选型规则
  • 在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类的特性进行选择,分析如下:
    1. 先判断存储的类型一组对象单列】或一组键值对双列】)
    2. .一组对象【单列】:Collection接口
      • 允许重复:List接口(Collection子接口)
        • 增删多:LinkedList【底层维护了一个双向链表】
        • 改查多:ArrayList【底层维护 Object类型的可变数组】
        • 多线程:Vector【synchronized修饰,线程安全】
      • 不允许重复:Set接口(Collection子接口)
        • 无序:HashSet【底层是HashMap,维护了一个哈希表 即[数组+链表+红黑树]】
        • 排序:TreeSet
        • 插入和取出顺序一致:LinkedHashSet【底层是LinkedHashMap(自己验证),维护 数组+双向链表】
        • 多线程:无有...【可以用Hashtable,只用键,值随意,看源码】
    3. 一组键值对【双列】:Map接口(和 Collection接口并列)
      • 键无序:HashMap【底层是:哈希表 jdk7: 数组+链表,jdk8: 数组+链表+红黑树】
      • 键排序:TreeMap
      • 键插入和取出顺序一致:LinkedHashMap【底层是HashMap】
      • 读取文件:Properties
      • 多线程:Hashtable或者Properties【Properties 是 Hashtable的子类】

TreeSet源码解读

public static void main(String[] args) {

    // TreeSet解读
    // 1.当我们使用无参构造器,创建TreeSet对象时,默认按字符串大小升序排序
    // 2.我们希望添加的元素,按照字符串大小排序
    // 3.使用TreeSet 提供的一个构造器,可以传入一个比较器(匿名内部类)
    //   并指定排序的规则

    // TreeSet treeSet = new TreeSet();
    TreeSet treeSet = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            String s1 = (String) o1;
            String s2 = (String) o2;
            // 下面 调用String的 compareTo方法进行字符串大小比较
            // return s1.compareTo(s2);
            // 如果要求加入的元素,按照String 长度大小排序
            return s1.length() - s2.length();
        }
    });
    // 添加数据
    treeSet.add("abcd");// OK
    treeSet.add("bcd");// OK
    treeSet.add("cd");// OK
    treeSet.add("d");// OK
    //treeSet.add("d");// 无法加入(s1.compareTo(s2) 与 s1.length() - s2.length()均无法加入)
    //treeSet.add("tom");// (s1.compareTo(s2)可以加入。s1.length() - s2.length()无法加入)

    System.out.println("treeSet=" + treeSet);
    // TreeSet源码解读
    // 1.构造器把传入的比较器对象,赋给了 TreeSet的底层的 TreeMap的一个属性this.comparator
    //      public TreeMap(Comparator<? super K> comparator) {
    //          this.comparator = comparator;
    //      }
    // 2.在 第一次调用 treeSet.add("abcd"), 在底层会执行
    //      if (t == null) {
    //          compare(key, key); // 检查Key 是否为 null
    //
    //          root = new Entry<>(key, value, null);
    //          size = 1;
    //          modCount++;
    //          return null;
    //      }
    // 3.在 第二次调用 treeSet.add("bcd"), 在底层会执行
    //
    //      if (cpr != null) {// cpr 就是我们的匿名内部类(对象)
    //          do {
    //              parent = t;
    //              // 动态绑定到我们传入的匿名内部类(对象)
    //              cmp = cpr.compare(key, t.key);
    //              if (cmp < 0)
    //                  t = t.left;
    //              else if (cmp > 0)
    //                  t = t.right;
    //              else // 如果相等,即比较后返回0,这个Key就没有加入,但是Value会被新值替换
    //                  return t.setValue(value);
    //          } while (t != null);
    //      }

}

TreeMap源码解读

public static void main(String[] args) {

    // 使用无参构造器(默认构造器), 创建TreeMap,
    // 默认调用String的compareTo方法, 按照Key升序排列
    // TreeMap treeMap = new TreeMap();
    TreeMap treeMap = new TreeMap(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            String s1 = (String) o1;
            String s2 = (String) o2;
            // 按照传入的Key(String) 的大小进行排序
            // return s1.compareTo(s2);
            // 按照Key(String) 的长度大小排序
            return s1.length() - s2.length();
        }
    });
    treeMap.put("a", "啊");
    treeMap.put("bcd", "不错的");
    treeMap.put("cd", "菜单");
    treeMap.put("def", "得分");

    System.out.println("treeMap=" + 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 相等,就不添加
    //                   // HashMap判断是否相等用 equals方法
    //                   // 而TreeMap判断是否相等用 compare方法
    //                  return t.setValue(value);
    //          } while (t != null);
    //      }
}

Collections工具类

Collections工具类介绍

  1. Collections 是一个操作Set、List和Map等集合的工具类.
  2. Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改操作

排序操作(均为static方法)

  1. reverse(List)反转List中元素的顺序.
  2. shuffle(List):对List集合元素进行随机排序.(抽奖)
  3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
  4. sort(List, Comparator):根据指定的Comparator 产生的顺序对 List集合元素进行排序
  5. swap(List, int, int):将指定List集合中的 i 处元素和 j 处元素进行交换.
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();
    arrayList.add("abc");
    arrayList.add("bc");
    arrayList.add("cde");
    arrayList.add("efgh");
    arrayList.add("f");
    System.out.println(arrayList);
    //1. reverse(List):反转List中元素的顺序.
    Collections.reverse(arrayList);
    System.out.println(arrayList);
    //2. shuffle(List):对List集合元素进行随机排序.(抽奖)
    System.out.println("===shuffle===");
    for (int i = 0; i < 5; i++) {
        Collections.shuffle(arrayList);
        System.out.println(arrayList);
    }
    //3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
    System.out.println("===自然排序===");
    Collections.sort(arrayList);
    System.out.println(arrayList);
    //4. sort(List, Comparator):根据指定的Comparator 产生的顺序对 List集合元素进行排序
    Collections.sort(arrayList, new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            String s1 = (String) o1;
            String s2 = (String) o2;
            return s1.length() - s2.length();
        }
    });
    System.out.println("===定制排序===");
    System.out.println(arrayList);
    //5. swap(List, int, int):将指定List集合中的 i 处元素和 j 处元素进行交换.
    Collections.swap(arrayList, 0, 4);
    System.out.println(arrayList);
}

查找、替换方法

  1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
  2. Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
  3. Object min(Collection)
  4. Object min(Collection)
  5. int frequency(Collection, Object):返回指定集合中指定元素的出现次数.
  6. void copy(List dest, List src):将src中的内容复制到dest中
  7. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值.
public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();
    arrayList.add("abc");
    arrayList.add("bc");
    arrayList.add("cde");
    arrayList.add("efgh");
    arrayList.add("f");
    arrayList.add("f");
    System.out.println(arrayList);
    
    // 1. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
    Object max = Collections.max(arrayList);
    System.out.println(max);
    //2. Object max(Collection, Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素
    max = Collections.max(arrayList, new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            String s1 = (String) o1;
            String s2 = (String) o2;
            return s1.length() - s2.length();
        }
    });
    System.out.println(max);
    //3. Object min(Collection)
    Object min = Collections.min(arrayList);
    System.out.println("min=" + min);
    //4. Object min(Collection)

    //5. int frequency(Collection, Object):返回指定集合中指定元素的出现次数.
    int num = Collections.frequency(arrayList, "f");
    System.out.println("num=" + num);

    //6. void copy(List dest, List src):将src中的内容复制到dest中
    ArrayList dest = new ArrayList();
    // 为了完成一个完整的拷贝,我们需要先给dest赋值,大小和src.size() 一样即可
    //  if (srcSize > dest.size())
    // 这里注意 size 与 容量的区别
    // 只有当集合中添加元素时,size才会增加
    // 在构造器中指定的大小是容量的大小,而不是size的大小
    // 因此即使在构造器中指定了大小,复制集合时依旧会抛出异常
    for (int i = 0; i < arrayList.size(); i++) {
        dest.add(null);
    }
    Collections.copy(dest, arrayList);
    System.out.println(dest);
    //7. boolean replaceAll(List list, Object oldVal, Object newVal):使用新值替换List对象的所有旧值.
    Collections.replaceAll(arrayList, "f", "FF");
    System.out.println(arrayList);
}

分析HashSet和TreeSet分别是如何实现去重的

(1) HashSet的去重机制: hashCode() + equals(), 底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table表索引所在的位置,没有数据,就直接放入。如果有数据,就进行equals比较[自己定义、遍历比较],如果比较后,不相同就加入,否则就不加入.

(2) TreeSet的去重机制:如果你传入了一个Comparator匿名对象,就使用实现的compare方法去重,如果方法返回 0,就认为是相同的元素/数据,就不添加。如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Comparable接口的compareTo方法去重。

TreeSet代码分析

@SuppressWarnings({"all"})
public class HomeWork05 {
    public static void main(String[] args) {
        TreeSet treeSet = new TreeSet();
        // 分析源码
        // add方法,因为TreeSet() 构造器没有传入Comparator接口的匿名内部类(对象)
        // 所有在底层 调用 compare(key, key); 
        //      final int compare(Object k1, Object k2) {
        //          return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        //              : comparator.compare((K)k1, (K)k2);
        //      }
        // ((Comparable<? super K>)k1).compareTo((K)k2) 即 把Person转成Comparable类型
        // 又因为我们的Person类 并没有实现 Comparable接口,所有会抛出ClassCastException
        // 解决方法:
        // 让Person类 实现(implements) Comparable 接口,重写compareTo方法 即可
        treeSet.add(new Person());// OK
        treeSet.add(new Person());// 加入不了
        treeSet.add(new Person());// 加入不了
        System.out.println(treeSet);// 只有一个对象
        
    }
}
class Person implements Comparable {
    @Override
    public int compareTo(Object o) {
        return 0;// 返回0,永远只能添加一个Person(与HashSet区分!!)
    }
}

HashSet代码分析

@SuppressWarnings({"all"})
public class HomeWork05 {
    public static void main(String[] args) {

        HashSet set = new HashSet();// OK
        Person p1 = new Person(1001, "AA");// OK
        Person p2 = new Person(1002, "BB");// OK
        set.add(p1);// OK
        set.add(p2);// OK
        System.out.println("===初始状态===");
        System.out.println(set);// 2个对象
        p1.name = "CC";
        // 这里删除不成功了,因为此时删除p1时,是按照当前的id和name来删除的.
        // 而 根据此时的id和name计算的hash值 与之前存放的p1的hash值是不同的.
        // 这里需要注意的是:(当你修改元素信息(内容)时,hash值是不会改变的(修改的), 仍然是以前的旧值)
        //
        // (e = removeNode(hash(key), key, null, false, true))
        // 这里会重新计算Key的hash值 hash(key) 根据新的hash值来删除元素
        set.remove(p1);// 无法删除以前的p1 !!! 删除时 也是根据(1)hash值和(2)equals 比较来进行删除的
        System.out.println("\n===set.remove(p1)后===");
        System.out.println(set);// 2个对象
        set.add(new Person(1001, "CC"));//OK (此时hash值与p1不同,但是equals比较相同)
        System.out.println("\n===add(new Person(1001, \"CC\"))后===");
        System.out.println(set);// 3个对象
        set.add(new Person(1001, "AA"));//OK (此时hash值为p1相同,但是equals比较不同)
        System.out.println("\n===add(new Person(1001, \"AA\"))后===");
        System.out.println(set);// 4个对象
    }
}
class Person {
    public int id;
    public String name;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id &&
                Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }

    @Override
    public String toString() {
        return "\nPerson{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

课后作业

单词

news 新闻.

title 标题.

content 内容.

processTitle 处理标题.

ConcurrentModificationException(并发修改异常)

Concurrent 并发.

Modification 修改.

关于java集合中元素的删除问题的详解

  • 在集合遍历(迭代)过程中,不允许元素通过集合删除,可以使用迭代器进行删除。(因为在集合进行迭代过程中,不允许集合的结构发生改变)
//-------------------------集合的删除元素方法--------------------------------------------
// 集合的删除元素方法 源码
//  public E remove(int index) {
//      rangeCheck(index);// 范围检查,检查index是否合理
//
//      modCount++;// 修改次数加1
//      E oldValue = elementData(index);// 根据索引index 获取旧值
//
//      int numMoved = size - index - 1;// 移动的距离 注意要 -1
//      if (numMoved > 0)
//			// 利用System.arraycopy()方法,来移动数组元素
//			// 将elementData数组中,从索引为index+1开始的元素,复制 numMoved个数据,
//			// 到同一个数组(elementData),索引为index开始,复制的数据个数为 numMoved.
//			// 最终效果:相当于将index 后面的数据向前移到了 1位
//    		// Arrays.copyOf() 返回一个新的数组,不能对原数组进行修改,无法实现移动元素
//			// System.arraycopy() 能够修改原数组中的值,可以实现移动元素
//          System.arraycopy(elementData, index+1, elementData, index,
//                          numMoved);
//		// 1.调节集合的长度,--size
//		// 2.将最后一个元素置为null,让GC工作,清除数据
//      elementData[--size] = null; // clear to let GC do its work
//		// 返回旧值
//      return oldValue;
//  }
// 范围检查方法,检查索引是否合理
//  private void rangeCheck(int index) {
//      if (index >= size)
//          throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//  }
// System类的arraycopy方法 源码
// 	public static native void arraycopy(Object src,  int  srcPos,
//                                        	Object dest, int destPos,
//                                        	int length);
//----------------------------Iterator的删除元素方法------------------------------------
// Iterator
// hasNext方法
//  public boolean hasNext() {
//		// cursor 指针
//      return cursor != size;
//  }
//	
// next方法
//  public E next() {
//		// 检查并发修改方法
//      checkForComodification();
//		// 此时的指针,指向本次要遍历的对象. cursor初值为0,没有 +1的情况下是第一个元素
//      int i = cursor;
//      if (i >= size)// 越界了
//          throw new NoSuchElementException();
//      Object[] elementData = ArrayList.this.elementData;
//      if (i >= elementData.length)
//          throw new ConcurrentModificationException();
//		// 指针指向了下一个元素,但i的值没有变
//      cursor = i + 1;
/
//		// lastRet 总是比 cursor 小1
//		// lastRet初始值为 -1
//		// cursor初始值为 0
//      return (E) elementData[lastRet = i];
//  }
// remove方法
//  public void remove() {
//		// 如果没有next()操作 就直接remove的话,lastRet = -1,会抛出异常
//      if (lastRet < 0)
//          throw new IllegalStateException();
//		// 检查并发修改方法
//      checkForComodification();
//
//      try {
//			// 调用集合的删除元素方法,根据下标索引删除
//          ArrayList.this.remove(lastRet);
//			// 指针回拨操作
//          cursor = lastRet;
//			// 将lastRet 重新置为-1
//          lastRet = -1;
//			// 手动将集合的modCount值 赋给 迭代器的expectedModCount
//			// ,防止出现oncurrentModificationException异常
//          expectedModCount = modCount;
//      } catch (IndexOutOfBoundsException ex) {
//          throw new ConcurrentModificationException();
//      }
//  }
//
// 检查并发修改 方法
//  final void checkForComodification() {
//      if (modCount != expectedModCount)
//      throw new ConcurrentModificationException();
//  }

正确删除集合中元素的方式-以ArrayList为例

单线程模式下(3种)

普通for

// 使用普通for循环,注意自己保证索引正常
public void test1() {
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i) == 3) {// 自动拆箱 "=="比较的是值 等价于 integer.equals(3);// 装箱
            list.remove(i);// 根据下标删除元素
            // 因为ArrayList 是动态数组,当调用remove方法删除元素时,后面的元素会 自动前移(自动的调整位置)
            // , 而我们的指针 i进行 i++,指针会后移,导致删除一个元素后会 漏掉下一个元素,因此我们需要指针回拨的操作
            i--;// 指针回拨

        }
    }
}

迭代器【推荐】

// 使用Iterator提供的remove方法,用于删除当前元素
public void test2() {
    // 执行 Iterator<Integer> iterator = list.iterator(); 时,相当于对集合拍了一个快照
    // ,迭代器进行迭代时会参照这个快照进行迭代。
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        Integer integer = iterator.next();
        if (integer == 3) {// 自动拆箱,"=="比较的是值 等价于 integer.equals(3);// 装箱
            // 1.这里使用的是迭代器的remove方法,如果使用集合的remove方法,会抛出异常
            // 2.
            iterator.remove();
            
            // 1.根据对象删除元素,这里不会 自动拆箱来进行根据索引删除元素
            // 2.
            // list.remove(integer); 
        }
    }
}
// 说明
// 1.利用迭代器进行删除操作(iterator.remove()),这样进行的删除操作会 同步到集合和迭代器,所以可以 实现正确的删除元素。
// 2.如果直接在集合中删除元素(list.remove(integer)),并没有通知迭代器,导致二者在遍历时不同步,抛出异常,无法实现和迭代器的集合元素相匹配。
// 结论:
// 1.在集合结构 发生变化时,必须重新获取迭代器。
// 2.在迭代元素的过程中,一定要使用 Iterator的remove方法删除元素,不要使用集合的删除方法。

image-20220125192314299.

// 以后补充

多线程模式下(1种)

// 暂未用到...以后补充

坦克大战2.0.

泛型(generic)

泛型的引入(泛型的理解和好处)

  • 使用传统方法的问题分析

    1. 不能对加入到集合 ArrayList中的数据类型进行约束(不安全)。
    2. 遍历的时候,需要进行类型转换,如果集合中的数据量较大,对效率有影响。
  • 使用泛型

    public static void main(String[] args) {
        // 使用泛型
        // 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 Cat("招财猫", 8));// 编译器报错
        for (Dog dog : arrayList) {
            System.out.println(dog.getName() + "-" + dog.getAge());
        }
    }
    
  • 泛型的好处

    1. 编译时,检查添加元素的类型,提高了安全性.

    2. 减少了类型转换的次数,提高效率.

      • 不使用泛型

        Dog -加入-> Object -取出-> Dog // 放入到ArrayList 会先转成Object,在取出时,还需要转换成Dog

      • 使用泛型

        Dog -加入-> Dog -取出-> Dog // 放入 和 取出时,不需要类型转换,提高效率.

    3. 不再提示编译警告

泛型介绍

int a = 10;
// 泛(广泛)型(类型): 可以这样理解 泛型就是表示数据类型的数据类型(即 可以接收数据类型的数据类型)
// E = Integer, String, Dog, MyDate
// 特别强调:E具体的数据类型在 定义对象的时候指定,即在编译期间,就确定E是什么类型
  1. 泛型又称为参数化类型,是JDK5.0 出现的新特性,解决数据类型的安全性问题.
  2. 在类声明或实例化时,只要指定好需要的具体的类型即可.
  3. Java 泛型可以保证如果程序在编译时没有发出警告运行时就不会产生ClassCastException类型转换异常【但是这里不能保证不会产生NullPointerException空指针异常,这需要程序员自己判断】。同时,代码更加简洁、健壮.
  4. 泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型.
public class Generic03 {
    public static void main(String[] args) {
        // 特别强调:T具体的数据类型在定义A对象的时候指定,即在编译期间,就确定T是什么类型
        AAA<String> a = new AAA<>("lemon");
        a.show();// String(class java.lang.String)
    }
}

class AAA<T> {
    // T表示 f的数据类型,该数据类型在定义A对象的时候指定,
    // 即在编译期间,就确定T是什么类型
    T f;// 标识属性
    public AAA(T f) {// 标识形参
        this.f = f;
    }
    public T f1() {// 标识返回值类型
        return f;
    }
    public void show() {
        System.out.println(f.getClass());// 显示运行类型
    }
}

泛型的语法

泛型的声明

interface 接口<T> {}class 类<K, V> {}.// 比如:List,ArrayList

说明:

  1. 其中 T, K, V 不代表值,而是表示类型
  2. 任意字母都可以。常用T表示,是Type的缩写

泛型的实例化

  • 要在类名后面指定 类型参数的值(Integer,String,Dog,MyDate)·`
    1. List<String> list = new ArrayList<String>();
    2. List<String> list = new ArrayList<>();【推荐】

泛型使用举例

HashMap<String, Student> hashMap = new HashMap<>(16);
// 添加数据
hashMap.put("jack", new Student(1, "jack"));
hashMap.put("tom", new Student(2, "tom"));
hashMap.put("marry", new Student(3, "marry"));

System.out.println("===entrySet===");
Set<Map.Entry<String, Student>> entrySet = hashMap.entrySet();
// Set<Map.Entry<String, Student>> 表示(2点)
// 1.集合中的数据是: 一个个的 Map.Entry<String, Student>
// 2.而Map.Entry 中包含的数据是 Key=String, Value=Student
//
//  public Set<Map.Entry<K,V>> entrySet() {
//      Set<Map.Entry<K,V>> es;
//      return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
//  }

for (Map.Entry<String, Student> entry : entrySet) {
    System.out.println(entry.getKey() + "-" + entry.getValue());
}

泛型使用的注意事项和细节

  1. interface List<T> {}, public class HashSet<E> {}..等等

    说明一下:T, E 只能是引用类型.

    // 给泛型指定数据类型时,要求是引用类型,不能是基本数据类型
    List<Integer> list1 = new ArrayList<Integer>();// OK
    List<int> list2 = new ArrayList<int>();// 错误
    
  2. 在给泛型指定具体类型后,可以传入该类型或者其子类类型.

    public class GenericDetail {
        public static void main(String[] args) {
            ArrayList<A> aList = new ArrayList<>();
            aList.add(new A());// OK
            aList.add(new B());// OK
            System.out.println(aList.get(0).getClass());// class com.lemon.generic.A
            System.out.println(aList.get(1).getClass());// class com.lemon.generic.B
            // aList.add(new C());// 错误
            // aList.add(new Object());// 错误
        }
    }
    class A {}
    class B extends A {}// B 是 A的子类
    class C {}
    
  3. 泛型的使用形式

    List<String> list1 = new ArrayList<String>();
    List<String> list2 = new ArrayList;//【推荐】编译器会进行类型推断
    
  4. 如果我们这样写 List list3 = new ArrayList(); 默认给他的 泛型是[<E> E 就是Object]

    List list = new ArrayList();
    // 上式等价于 List<Object> list1 = new ArrayList<>();
    

泛型课堂练习( 比较字符串和比较出生年月 )值得学习

初始版本(麻烦)
public static void main(String[] args) {
    ArrayList<Employee> list = new ArrayList<>();
    // 添加数据
    list.add(new Employee("tom", 1000, new MyDate(2001, 1, 1)));
    list.add(new Employee("tom", 100, new MyDate(2003, 12, 16)));
    list.add(new Employee("tom", 500, new MyDate(2003, 12, 12)));
    list.add(new Employee("smith", 10000, new MyDate(2020, 5, 5)));
    System.out.println("===初始数据遍历 增强for(iter)===");
    for (Employee employee : list) {
        System.out.println(employee);
    }
    
    list.sort(new Comparator<Employee>() {
        @Override
        public int compare(Employee o1, Employee o2) {
            // 自己写的,一眼看上去很乱,界面不简洁
            // 名字判断很复杂,很死板
            // 年月日判断也有点复杂,还需要继续学习
            if (!o1.getName().equals(o2.getName())) {
                return o1.getName().compareTo(o2.getName());
            } else {
                if (o1.getBirthday().getYear() > o2.getBirthday().getYear()) {
                    return 1;
                } else if (o1.getBirthday().getYear() < o2.getBirthday().getYear()) {
                    return -1;
                } else {
                    if (o1.getBirthday().getMonth() > o2.getBirthday().getMonth()) {
                        return 1;
                    } else if (o1.getBirthday().getMonth() < o2.getBirthday().getMonth()) {
                        return -1;
                    } else {
                        if (o1.getBirthday().getDay() > o2.getBirthday().getDay()) {
                            return 1;
                        } else if (o1.getBirthday().getDay() < o2.getBirthday().getDay()) {
                            return -1;
                        } else {
                            return 0;
                        }
                    }
                }
            }
        }
    });
    System.out.println("===排序之后的数据遍历 迭代器(itit)===");
    Iterator<Employee> iterator = list.iterator();
    while (iterator.hasNext()) {
        Employee employee = iterator.next();
        System.out.println(employee);
    }
}
class Employee {
    private String name;
    private double sal;
    private MyDate birthday;
	// ......
}
class MyDate {
    private int year;
    private int month;
    private int day;
    // ......
}
升级版本(比较简洁)
// 没有利用封装
list.sort(new Comparator<Employee>() {
    @Override
    public int compare(Employee emp1, Employee emp2) {
        // 判空处理
        if (emp1 == null || emp2 == null) {
            System.out.println("Employee 不能为null");
            return 0;
        }
        // 名称比较
        int i = emp1.getName().compareTo(emp2.getName());
        if (i != 0) {
            return i;
        }
        // 下面是出生年月日比较
        // 比较年
        int year = emp1.getBirthday().getYear() - emp2.getBirthday().getYear();
        if (year != 0) {
            return year;
        }
        // 比较月
        int month = emp1.getBirthday().getMonth() - emp2.getBirthday().getMonth();
        if (month != 0) {
            return month;
        }
        // 这里不能太死板
        // 比较日时,直接返回
        return emp1.getBirthday().getDay() - emp2.getBirthday().getDay();
    }
});
最终版(最简洁)要学习,借鉴
// 利用了封装
list.sort(new Comparator<Employee>() {
    @Override
    public int compare(Employee emp1, Employee emp2) {
        // 判空处理
        if (emp1 == null || emp2 == null) {
            System.out.println("Employee 不能为null");
            return 0;
        }
		// 自己写的判断名字的程序
        // 	if (!o1.getName().equals(o2.getName())) {
        // 		return o1.getName().compareTo(o2.getName());
        // 	}
        
        // 这里没有用equals方法来判断名字,而是直接用String的compareTo方法
        // 如果compareTo方法返回0,则说明String相等 即 equals方法 返回true
        // 反之 返回相应数值,按照字符串的大小排序
        int i = emp1.getName().compareTo(emp2.getName());
        if (i != 0) {
            return i;
        }
        // 调用MyDate的compareTo方法
        return emp1.getBirthday().compareTo(emp2.getBirthday());
    }
});

class MyDate implements Comparable<MyDate>{
    private int year;
    private int month;
    private int day;

    @Override
    public int compareTo(MyDate o) {// 比较出生年月日的方法
        // 比较year
        int yearMinus = year - o.year;
        if (yearMinus != 0) {
            return yearMinus;
        }
        // 比较month
        int monthMinus = month - o.month;
        if (monthMinus != 0) {
            return monthMinus;
        }
        // 比较day
        return day - o.day;
    }
}

练习总结

Comparable接口的使用

Comparable 可比较的.

// Comparable接口源码(只有这一个方法)
public interface Comparable<T> {
    public int compareTo(T o);// 返回值类型 int
}
// 1.使用场景:
// 让需要比较的类实现Comparable接口,那么该类就要实现compareTo()方法
// 通常是 让类实现(implements)该接口(Comparable接口) 
// class AAA implements Comparable<T> {}
// 2.注意事项:
// (1)形参个数为1个
// (2)实现Comparable接口时注意要指定泛型T
Comparator接口的使用

Comparator 比较器.

// Comparator接口源码(还有很多方法)
public interface Comparator<T> {
    int compare(T o1, T o2);// 返回值类型也是 int
}
// 1.使用场景:
// 常用作Comparator匿名内部类对象, 用在sort()方法中, 进行定制排序
// 通常是使用 它的匿名内部类(new Comparator<T>() {})
// 2.注意事项:
// (1)形参个数为2个,
// (2)实现Comparator接口(使用匿名内部类)时注意要指定泛型T
compareTo方法和compare方法返回值的处理
利用 != 来代替 >0 和 <0 的两种情况
// compareTo方法和compare方法的返回值都是整数int
// 利用 != 来代替 >0 和 <0 的两种情况
// 比较的次数减少了,代码也更加的简洁
if (n1 > n2) {// nMinus = n1 - n2 > 0
    return 1;
} else if (n1 < n2) {// nMinus = n1 - n2 < 0
    return -1;
} else {
    return 0;
}
// 上式等价于,强烈推荐学习
int nMinus = n1 - n2;
if (nMinus != 0) {
    return nMinus;
} else {
    return 0;
}

// 同理可得
if (n1 > n2) {// nMinus = n1 - n2 > 0 ==> nMinus2 = n2- n1 < 0
    return -1;
} else if (n1 < n2) {// nMinus = n1 - n2 < 0 ==> nMinus2 = n2- n1 >0
    return 1;
} else {
    return 0;
}
// 上式又等价于 
int nMinus = n1 - n2;
if (nMinus != 0) {
    return -nMinus;
} else {
    return 0;
}
// 另外一种等价方式 
int nMinus2 = n2 - n1;
if (nMinus2 != 0) {
    return nMinus2;
} else {
    return 0;
}

自定义泛型(CustomGeneric)

自定义泛型类(CustomClassGeneric)

基本语法

class 类名<T, R...> {// ...表示可以有多个泛型
    // 成员(属性+方法)
}

注意细节

  1. 普通成员可以使用泛型(属性、方法).
  2. 使用泛型的数组不能初始化只能声明定义.
  3. 使用泛型的属性不能初始化只能声明定义.
  4. 使用泛型的集合既能够声明,也能够初始化.
  5. 静态方法不能使用类的泛型.
  6. 泛型类的类型(即 T,R...),是在创建对象时确定的(因为创建对象时,需要指定确定的类型).
  7. 如果在创建对象时,没有指定类型,默认是Object.

应用实例

// 自定义泛型类 解读
// 1.Tiger 后面有泛型,所有我们把Tiger 就称为自定义泛型类
// 2.T, R, M 泛型的标识符,一般是单个大写字母
// 3.泛型的标识符可以有多个
// 4.普通成员可以使用泛型(属性、方法)
// 5.getter 和 setter 是根据 属性名称 来命名的,而 不是 根据属性的数据类型命名的.
//   因为这个例子 使用的泛型 和 属性名称一致,容易误以为是根据数据类型来命名的,这是不对的.
//   getter 和 setter 方法就是根据 属性名称 命名的!!!
// 6.使用泛型的数组,不能初始化,只能声明定义
// 7.使用泛型属性同样不能初始化,只能声明定义
// 8.使用泛型集合即可以声明,也可以初始化
// 9.静态成员(属性和方法) 中不能使用类的泛型
class Tiger<T, R, M> {
    // (4.1)属性使用泛型
    T tttt;
    // T tttt = 10;// 泛型属性也不能初始化
    R r;
    // 因为数组在new 的时候,不能确定M的类型,不知道需要多大的内存,就无法在内存开辟空间
    // 可以定义,但是不能初始化
    //M[] m = new M[8];// 不能初始化
    M[] m;// 能够定义
    // 使用泛型集合即可以声明,也可以初始化
    List<R> list = new ArrayList<>();
    Map<T, R> map = new HashMap<>();

//    {
//        m = new M[9];// 泛型数组不能初始化
//    }
    // (4.2)构造器使用泛型
    public Tiger(T tttt, R r, M[] m) {
        this.tttt = tttt;
        this.r = r;
        // this.m = new M[10];// 泛型数组不能初始化
        this.m = m;
    }

    // 因为静态(static)是和类相关的,在类加载时,对象还没有创建
    // 泛型是在对象创建时指定的,所以,如果静态方法和静态属性使用了泛型,JVM就无法完成初始化
//    static R r2;
//    public static void f1(T t) {
//
//    }

    // (4.3)方法使用到泛型
    //
    public T getTttt() {
        return tttt;
    }

    public void setTttt(T tttt) {
        this.tttt = tttt;
    }

    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;
    }
}

自定义泛型接口(CustomInterfaceGeneric)

基本语法

interface 接口名<T, R...> {
    
}

接口的基本知识.

注意细节

  1. 接口中,静态成员也不能使用泛型(这个和泛型类规定一样).
  2. 泛型接口的类型(即 T, R...),在继承接口或者实现接口时确定.
  3. 没有指定类型,默认为Object.

应用实例

// 泛型接口使用的说明
// 1.接口中,静态成员也不能使用泛型
// 2.泛型接口的类型,在继承接口或者实现接口时确定
// 3.没有指定类型,默认Object

// 在继承接口 时指定泛型接口的类型
interface IA extends IUsb<String, Double> {

}
// 当我们去实现IA接口时,因为IA 在继承 IUsb接口时,指定了 U 为String, R为Double
// 因此,在实现IUsb接口的方法时,使用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;// 等价于 public static final int n = 10;

    // 接口中的属性默认是 public static final
    // U u;// 因此不能这样使用

    // 普通方法中,可以使用接口泛型
    R get(U u);

    void hi(R r);

    void run(R r1, R r2, U u1, U u2);

    // 在jdk8 中,可以在接口中,使用默认方法,也是可以使用泛型
    // default 关键字要显示定义,不能不写
    default R method(U u) {
        return null;
    }

}

自定义泛型方法(CustomMethodGeneric)

基本语法

修饰符 <T, R...> 返回类型 方法名(形参列表) {
    
}

注意细节

  1. 泛型方法,可以定义在普通类中,也可以定义在泛型类中.
  2. 当泛型方法被调用时,类型会确定.
  3. public void eat(E e) {} 修饰符后没有 <T, R...>,所以eat方法不是泛型方法而是使用了泛型.
  4. 泛型方法,可以使用类声明的泛型,也可以使用自己声明的泛型.

应用实例

// 泛型方法,可以定义在普通类中,也可以定义在泛型类中
class Car {// 普通类

    public void run() {// 普通方法
    }
    // 泛型方法说明
    // 1.<T, R> 就是泛型(泛型标识符)
    // 2.<T, R> 是提供给fly方法使用的
    // 3.当泛型方法被调用时,根据传入的参数,编译器就会确定泛型的类型(即 确定<T, R...>的类型)
    public <T, R> void fly(T t, R r) {// 泛型方法
        System.out.println(t.getClass().getSimpleName());
        System.out.println(r.getClass());
    }
}

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(K k, T t) {
        System.out.println(k.getClass());
        System.out.println(t.getClass());
    }
}

泛型的继承和通配符

泛型的继承和通配符说明

  1. 泛型不具备继承性

    List<Object> list = new ArrayList<String>();// 错误
    
  2. <?>:支持任意泛型类型

  3. <? extends A>:支持A类以及A类的子类,规定了泛型的上限.

  4. <? super A>:支持A类以及A类的父类,不限于直接父类,规定了泛型的下限.

实例代码

public class GenericExtends {
    public static void main(String[] args) {
        Object o = new String("java");
        // 泛型没有继承性
        // List<Object> list = new ArrayList<String>();// 错误
        ArrayList<Object> list1 = new ArrayList<>();
        ArrayList<String> list2 = new ArrayList<>();
        ArrayList<AA> list3 = new ArrayList<>();
        ArrayList<BB> list4 = new ArrayList<>();
        ArrayList<CC> list5 = new ArrayList<>();
        printCollection1(list1);
        printCollection1(list2);
        printCollection1(list3);
        printCollection1(list4);
        printCollection1(list5);
        System.out.println("============");
        //printCollection2(list1);// ×
        //printCollection2(list2);// ×
        printCollection2(list3);
        printCollection2(list4);
        printCollection2(list5);
        System.out.println("============");
        printCollection3(list1);
        //printCollection3(list2);// ×
        printCollection3(list3);
        //printCollection3(list4);// ×
        //printCollection3(list5);// ×
        
    }

    // 说明:List<?> 表示 任意的泛型类型都可以接受
    public static void printCollection1(List<?> list) {
        for (Object obj : list) {// 通配符,取出时,即使Object
            System.out.println(obj);
        }
    }

    // ? extends AA:表示 上限,可以接受 AA或者AA的子类
    public static void printCollection2(List<? extends AA> list) {
        for (AA aa : list) {
            System.out.println(aa);
        }
    }

    // ? super 子类类名AA:支持AA类以及AA类的父类,不限于直接父类
    // 规定了泛型的下限
    public static void printCollection3(List<? super AA> list) {
        for (Object obj : list) {
            System.out.println(obj);
        }
    }
}
class AA {}
class BB extends AA {}
class CC extends BB {}

JUnit

为什么需要JUnit

  1. 一个类有很多功能代码需要测试,为了测试,就需要写入到main方法中.
  2. 如果有多个功能代码测试,就需要来回注销,切换很麻烦.
  3. 如果可以直接运行一个方法,就方便很多,并且可以给出相关信息,就好了 --> JUnit.

基本介绍

  1. JUnit 是一个Java 语言的单元测试框架.
  2. 多数Java 的开发环境都已经集成了JUnit作为单元测试的工具.

注意事项

  1. 现在常用的JUnitJUnit5 版本.
  2. @Test 注解用在 普通 无参方法,静态方法,有参方法都不能使用.

坦克大战1.0

java绘图坐标体系

坐标体系-介绍

  • 下图说明了Java坐标系。坐标原点位于左上角,以像素为单位。在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。
  • image-20220118211359249.

坐标体系-像素

  1. 绘图还必须要搞清一个非常重要的概念 -- 像素 一个像素等于多少厘米?答:无法比较
  2. 计算机在屏幕上显示的内容都是由屏幕上的每 一个像素组成的。例如,计算机显示器的分辨率是800×600,表示计算机屏幕上的每一行由800个点组成,共有600行,整个计算机屏幕共有480 000个像素。像素是一个密度单位,而厘米是长度单位,两者无法比较.

java绘图技术

绘图的基本框架代码

  • image-20220119101619088.
// 绘图的框架代码
// 演示如何在面板上画出圆形
public class DrawCircle extends JFrame {// JFrame对应一个窗口,可以理解成是一个画框

    // 定义一个面板
    private MyPanel myPanel = null;

    public static void main(String[] args) {
        new DrawCircle();
    }
    
    public DrawCircle() {// 构造器
        // 初始化面板
        myPanel = new MyPanel();
        // 把面板放入到窗口(画框)
        this.add(myPanel);
        // 设置窗口的大小
        this.setSize(400, 300);
        // 当点击窗口的小×,程序完全退出.
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 可以显示
        this.setVisible(true);
    }
}


// 1.先自定义一个面板 MyPanel, 继承JPanel类, 画图形,就在面板上画
class MyPanel extends JPanel {

    // 重写paint方法
    // 1.MyPanel 对象就是一个画板(面板)
    // 2.Graphics g 把 g 理解成一支画笔
    // 3.Graphics 提供了很多绘图的方法
    @Override
    public void paint(Graphics g) {// 绘图方法
        super.paint(g);// 调用父类的方法完成初始化,一定要保留!!
        System.out.println("paint方法");
        // 画一个圆形
        g.drawOval(100, 0, 100, 100);
    }
}

绘图原理

Component 组成部分,组件.

  • Component类提供了两个和绘图相关的重要的方法:
    1. paint(Graphics g)绘制组件的外观。
    2. repaint()刷新组件的外观。
  • 当组件第一次在屏幕显示的时候,程序会自动的调用paint() 方法来绘制组件。
  • 在以下情况paint() 将会被调用:
    1. 窗口最小化,再最大化.
    2. 窗口的大小发生变化.
    3. repaint方法被调用
  • paint() 方法有 4种情况会被调用,第一次是自动调用。

Graphics类的常用方法

  • Graphics类 你可以理解成就是画笔,为我们提供了各种绘制图像的方法
// 1.画直线 drawLine()
g.drawLine(10, 10, 100, 100);
// 2.画矩形边框 drawRect
g.drawRect(10, 10, 100, 100);
// 3.画椭圆边框 drawOval
g.drawOval(10, 10, 100, 50);

// 4.填充矩形 fillRect
// 设置画笔颜色
g.setColor(Color.CYAN);
g.fillRect(10, 10, 100, 100);

// 5.填充椭圆 fillOval
g.setColor(Color.MAGENTA);
g.fillOval(10, 10, 100, 100);

// 6.画图片 drawImage
// 获取(加载)图片资源, /tj.png 表示在该项目的 根目录去获取 tj.png 图片
Image image = Toolkit.getDefaultToolkit().getImage(Panel.class.getResource("/tj.png"));
g.drawImage(image, 10, 10, 200, 200, this);

// 7.画字符串(写字) drawString
// 给画笔设置颜色和字体
g.setColor(Color.green);
g.setFont(new Font("宋体", Font.BOLD, 50));
// 这里设置的 100 100,是 "软柠柠吖"的 左下角
// (不是左上角了,注意一下,就字符串比较特殊是左下角,其他的都是左上角)
g.drawString("软柠柠吖", 100, 100);

// 8.设置画笔的字体 setColor
// 9.设置画笔的颜色 setFont

java事件处理机制

event 事件.

小球移动案例代码

// 演示小球通过键盘控制上下左右移动-> 讲解Java的事件控制
public class BallMove extends JFrame {// 窗口

    private MyPanel myPanel = null;

    public static void main(String[] args) {
        new BallMove();
    }

    public BallMove() {
        myPanel = new MyPanel();
        this.add(myPanel);
        this.setSize(1000, 750);
        this.setLocation(200, 200);
        // 窗口JFrame 对象可以监听键盘事件,即可以监听到面板发生的键盘事件
        this.addKeyListener(myPanel);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

// 面板, 可以画小球
// KeyListener 是一个监听器,可以监听键盘事件
class MyPanel extends JPanel implements KeyListener {


    // 为了让小球可以移动,把他的左上角的坐标(x, y)设置成变量
    int x = 10;
    int y = 10;

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillOval(x, y, 100, 100);// 默认黑色
    }

    // 监听字符输入
    // 有字符输出时,该方法就会被触发
    @Override
    public void keyTyped(KeyEvent e) {

    }

    // 当某个键按下时,该方法就会被触发
    @Override
    public void keyPressed(KeyEvent e) {

        // System.out.println((char)e.getKeyCode() + "被按下...");

        // 根据用户按下的不同的键,来处理小球的移动(上下左右的键)
        // 在java中,会给每一个键,分配一个值(int)
        if (e.getKeyCode() == KeyEvent.VK_DOWN) {// KeyEvent.VK_DOWN就是向下的箭头对应的code
            // y++;
            y += 5;
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            // y--;
            y -= 5;
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            // x++;
            x += 5;
        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            // x--;
            x -= 5;
        }

        // 让面板重绘
        this.repaint();
    }

    // 当某个键释放(松开)时,该方法被触发
    @Override
    public void keyReleased(KeyEvent e) {

    }
}

基本说明

java事件处理是采用"委派事件模型"。当事件发生时,产生事件的对象,会把此"信息"传递给"事件的监听者"处理,这里所说的"信息"实际上就是 java.awt.event 事件类库里某个类所创建的对象,把它称为"事件的对象".

  • 示意图

image-20220120112346390.

事件处理机制深入理解

  • 前面我们提到几个重要的概念 事件源事件事件监听器,我们下面来全面的介绍他们.

    1. 事件源:事件源是一个产生事件的对象,比如按钮,窗口等.
    2. 事件:事件就是承载事件源状态改变时的对象,比如当键盘事件、鼠标事件、窗口事件等等,会产生一个事件对象,该对象保存着当前事件的很多信息,比如 KeyEvent 对象含有被按下的键的Code值。java.awt.event包和javax.swing.event包中定义了各种事件类型.

    image-20220120111251841.

    image-20220120111337917.

    1. 事件监听器接口

      1. 当事件源产生一个事件,可以传送给事件监听者处理.
      2. 事件监听者实际上就是一个类,该类实现了某个事件监听器接口。比如前面的案例中的MyPanel就是一个类,它实现了KeyListener接口,它就可以作为一个事件监听者,对接受到的事件进行处理.
      3. 事件监听器接口有多种,不同的事件监听器接口可以监听不同的事件,一个类可以实现多个监听接口.
      4. 这些接口在 java.awt.event包和 javax.swing.event包中定义。

      image-20220120110315149.

坦克大战1.0示例代码

父类坦克-Tank

package com.lemon.tankgame;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class Tank {// 坦克父类
    private int x;// 坦克的横坐标
    private int y;// 坦克的纵坐标
    private int direct;// 坦克方向 0上 1右 2下 3左
    private int type = 0;
    private int speed = 1;

    public Tank(int x, int y, int direct, int type) {
        this.x = x;
        this.y = y;
        this.direct = direct;
        this.type = type;
    }
    // 上右下左移动的方法
    public void moveUp() {
        y -= speed;
    }
    public void moveRight() {
        x += speed;
    }
    public void moveDown() {
        y += speed;
    }
    public void moveLeft() {
        x -= speed;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getDirect() {
        return direct;
    }

    public void setDirect(int direct) {
        this.direct = direct;
    }

    public int getSpeed() {
        return speed;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public void setSpeed(int speed) {
        if (speed <= 0 || speed > 10) {
            System.out.println("速度范围在1~10之间,默认为1");
            return;
        }
        this.speed = speed;
    }
}

我方子类坦克-Hero

package com.lemon.tankgame;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class Hero extends Tank {

    public Hero(int x, int y, int direct, int type) {
        super(x, y, direct, type);
    }
}

敌方子类坦克-Enemy

package com.lemon.tankgame;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class Enemy extends Tank {

    public Enemy(int x, int y, int direct, int type) {
        super(x, y, direct, type);
    }
}

画板类-MyPanel

package com.lemon.tankgame;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Vector;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 * 坦克大战的绘图区域
 */
// 为了监听 键盘事件,我们要实现KeyListener
public class MyPanel extends JPanel implements KeyListener {
    // 定义我的坦克
    Hero hero = null;
    // 敌人坦克小组
    // 考虑多线程问题 用集合Vector
    Vector<Enemy> enemies = new Vector<>();
    // Enemy[] enemies = new Enemy[3];
    int enemyTankSize = 3;

    public MyPanel() {
        hero = new Hero(110, 200, 0, 0);// 初始化自己的坦克
        hero.setSpeed(9);

        for (int i = 0; i < enemyTankSize; i++) {
            enemies.add(new Enemy(100 * (i + 1), 30, 2, 1));
        }

        //enemies.add(new Enemy(100, 30, 2, 1));
        //enemies.add(new Enemy(200, 30, 2, 1));
        //enemies.add(new Enemy(300, 30, 2, 1));


        //enemies[0] = new Enemy(100, 30, 2, 1);
        //enemies[1] = new Enemy(200, 30, 2, 1);
        //enemies[2] = new Enemy(300, 30, 2, 1);
    }

    @Override
    public void paint(Graphics g) {
        super.paint(g);
        // 游戏区域背景(默认为黑色)
        g.fillRect(0, 0, 1000, 750);

        // 画出坦克-封装方法
        // drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), hero.getType());
        madeTank(hero, g);

        for (Enemy enemy : enemies) {
            madeTank(enemy, g);
        }

        //madeTank(enemies.get(0), g);
        //madeTank(enemies.get(1), g);
        //madeTank(enemies.get(2), g);

        //madeTank(enemies[0], g);
        //madeTank(enemies[1], g);
        //madeTank(enemies[2], g);

    }

    public void madeTank(Tank tank, Graphics g) {
        drawTank(tank.getX(), tank.getY(), g, tank.getDirect(), tank.getType());
    }

    /**
     * 编写方法,画出坦克
     *
     * @param x      坦克左上角x坐标
     * @param y      坦克左上角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;
            default:
                g.setColor(Color.pink);
                break;
        }

        // 根据坦克方向,来绘制对应形状的坦克
        switch (direct) {
            case 0:// 方向朝上
                // 坦克左轮
                g.fill3DRect(x - 20, y - 30, 10, 60, false);
                // 坦克右轮
                g.fill3DRect(x + 10, y - 30, 10, 60, false);
                // 坦克中间
                g.fill3DRect(x - 10, y - 20, 20, 40, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x, y - 10, x, y - 35);
                break;
            case 1:// 方向朝右
                // 坦克左轮
                g.fill3DRect(x - 30, y - 20, 60, 10, false);
                // 坦克右轮
                g.fill3DRect(x - 30, y + 10, 60, 10, false);
                // 坦克中间
                g.fill3DRect(x - 20, y - 10, 40, 20, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x + 10, y, x + 35, y);
                break;
            case 2:// 方向朝下
                // 坦克左轮
                g.fill3DRect(x - 20, y - 30, 10, 60, false);
                // 坦克右轮
                g.fill3DRect(x + 10, y - 30, 10, 60, false);
                // 坦克中间
                g.fill3DRect(x - 10, y - 20, 20, 40, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x, y + 10, x, y + 35);
                break;
            case 3:// 方向朝左
                // 坦克左轮
                g.fill3DRect(x - 30, y - 20, 60, 10, false);
                // 坦克右轮
                g.fill3DRect(x - 30, y + 10, 60, 10, false);
                // 坦克中间
                g.fill3DRect(x - 20, y - 10, 40, 20, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x - 10, y, x - 35, y);
                break;
            default:
                break;
        }
    }

    @Override
    public void keyTyped(KeyEvent e) {

    }

    // 处理wasd 键按下的情况
    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_S) {// 按下S键
            hero.setDirect(2);// 改变坦克的方向
            hero.moveDown();// 坦克向下移动
        } else if (e.getKeyCode() == KeyEvent.VK_W) {// 按下W键
            hero.setDirect(0);
            hero.moveUp();
        } else if (e.getKeyCode() == KeyEvent.VK_A) {// 按下A键
            hero.setDirect(3);
            hero.moveLeft();
        } else if (e.getKeyCode() == KeyEvent.VK_D) {// D键
            hero.setDirect(1);
            hero.moveRight();
        }

        // 让面板重绘
        repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }
}

程序入口类-LemonTankGame01

package com.lemon.tankgame;

import javax.swing.*;


/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class LemonTankGame01 extends JFrame {
    private MyPanel myPanel = null;

    public static void main(String[] args) {
        new LemonTankGame01();
    }

    public LemonTankGame01() {
        myPanel = new MyPanel();
        this.add(myPanel);// 把面板(就是游戏的绘图区域)
        this.addKeyListener(myPanel);// 让JFrame 监听myPanel的键盘事件
        this.setSize(1000, 750);
        this.setLocation(200, 200);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

线程

线程相关概念

程序(program)

  • 程序是为完成特定任务、用某种语言编写的一组指令的集合。
  • 简单的说:就是我们写的代码.静态的

进程

  • 进程是指运行中的程序,比如我们使用QQ,就启动了一个进程操作系统就会为该进程 分配内存空间。当我们使用迅雷时,又启动了一个进程操作系统将为迅雷分配新的内存空间
  • 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自己的产生、存在和消亡过程.

线程

  • 线程由进程创建的,是进程的一个实体.
  • 一个进程可以拥有多个线程.

单线程

  • 同一个时刻,只允许 执行一个线程.

多线程

  • 同一个时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口;一个迅雷进程,可以同时下载多个文件。
程序、进程、线程示意图

image-20220120193348340.

并发

  • 并发:同一个时刻,多个任务交替执行,造成一种 "貌似同时" 的错觉,简单的说,单核cpu实现的多任务就是并发.

image-20220121191244194.

并行

  • 并行:同一个时刻,多个任务同时执行多核cpu可以实现并行.
  • 并发和并行可以同时存在

image-20220121103722874.

并发和并行同时存在

image-20220121191323303.

查看电脑CPU个数 示例代码

available 可获得的.

Processors 处理器.

public class CpuNum {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();// 单例模式
        // 获取当前电脑的cpu数量/核心数
        int cpuNums = runtime.availableProcessors();
        System.out.println("当前电脑cpu 个数=" + cpuNums);
    }
}

线程基本使用

创建线程的两种方式

  • Java中线程使用的两种方法(创建的 2 中方式)
    1. 继承Thread类,重写run方法.
    2. 实现Runnable接口,重写run方法.

线程示意图

image-20220120193931735.

线程应用案例1-继承Thread类

示例代码

package com.lemon.threaduse;

/**
 * @author 软柠柠吖
 * @date 2022/1/20
 * 演示通过继承Thread类,创建线程
 */
public class Thread01 {
    public static void main(String[] args) throws InterruptedException {

        // 创建Cat对象,可以当做线程使用
        Cat cat = new Cat();

        // 读源码
        // 1.start方法
        //  public synchronized void start() {
        //      start0();
        //  }
        // 2.start0
        //   (1)start0() 是本地方法,由JVM调用,底层是c/c++实现
        //   (2)真正实现多线程的效果,是start0(), 而不是run
        //  private native void start0();

        cat.start();// 启动线程-> 最终会执行cat的run方法
        // cat.run();// run方法就是一个普通的方法,没有真正的启动一个线程,就会把run方法执行完毕(主线程阻塞),才向下执行
        // 说明:当main线程启动一个子线程Thread-0,主线程不会阻塞,会继续执行
        // 这时,主线程和子线程是交替执行.


        System.out.println("主线程继续执行...主线程名=" + Thread.currentThread().getName());
        for (int i = 0; i < 6; i++) {
            System.out.println("主线程 i=" + i);
            // 让主线程休眠
            Thread.sleep(1000);
        }
    }
}

// 说明
// 1.当一个类继承了 Thread类,该类就可以当做 线程使用
// 2.我们会重写run方法,写上自己的业务代码
// 3.run方法是 Thread 类 实现了 Runnable 接口的 run方法的重写方法
//   Thread类的run方法源码
//      @Override
//      public void run() {
//          if (target != null) {
//               target.run();// 动态绑定
//          }
//      }
class Cat extends Thread {
    int count = 0;

    @Override
    public void run() {// 重写run方法,写上自己的业务逻辑
        while (true) {
            System.out.println("喵喵,我是小猫咪" + (++count) + " 线程名=" + Thread.currentThread().getName());
            try {
                // 让该线程休眠1秒 (1s = 1000ms)
                // ctrl+alt+t 快捷键
                Thread.sleep(1000);// millis:毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                break;// 当count 到5,退出while循环,这时线程也就退出
            }
        }
    }
}

多线程机制

image-20220121122850932.

image-20220121122721773.

为什么是start

参照 线程应用案例1-继承Thread类.

线程应用案例2-实现Runnable接口

基本说明

  1. Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类的方法来创建线程显然不可能了.
  2. java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口创建线程

示例代码

package com.lemon.threaduse;

/**
 * @author 软柠柠吖
 * @date 2022/1/20
 * 通过实现接口Runnable 来开发线程
 */
public class Thread02 {
    public static void main(String[] args) throws InterruptedException {
        Dog dog = new Dog();
        // dog.run();// 普通方法,并没有实现多线程
        // dog.start();// 这里不能调用start

        // 创建Thread对象,把dog对象(实现了Runnable),放入Thread
        Thread thread = new Thread(dog);
        thread.start();

        System.out.println("主方法继续...");
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程 i=" + i + " 主线程名=" + Thread.currentThread().getName());
            Thread.sleep(1000);
        }
    }
}

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);// 休眠1s
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 5) {
                break;
            }
        }// while
    }// run()
}

代理模式【静态代理】- 代码演示

Proxy 代理.

// 代理模式 => 代码模拟-实现Runnable接口 开发线程的机制
// 线程代理类,模拟了一个极简的Thread -- 静态代理
class ThreadProxy implements Runnable {// 把ThreadProxy类当做Thread

    private Runnable target = null;// 属性 类型是Runnable

    public ThreadProxy(Runnable target) {
        this.target = target;
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();// 动态绑定(运行类型 Tiger)
        }
    }

    public void start() {
        start0();// start0方法 是真正实现多线程的方法
    }

    public void start0() {
        run();
    }
}

class Animal { }

class Tiger extends Animal implements Runnable {
    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫...");
    }
}

// 主方法
public static void main(String[] args) throws InterruptedException {
    Tiger tiger = new Tiger();// 实现了 Runnable接口
    ThreadProxy proxy = new ThreadProxy(tiger);
    proxy.start();
}

接口的多态特性.

如何理解线程

image-20220121154302222.

继承Thread VS 实现Runnable的区别

  1. Java的设计来看,通过继承 Thread 或者实现 Runnable 接口来创建线程本质上没有区别,从jdk帮助文档我们可以看到Thread类本身就实现了 Runnable 接口. start() --> start0().
  2. 实现 Runnable 接口方式更加适合多个线程共享一个资源的情况,并且避免了单继承的限制建议使用Runnable接口
实现Runnable共享资源【推荐】
T3 t3 = new T3("hello ");// T3实现了(implements) Runnable接口
// 让两个线程来执行t3的run方法.
// 两个线程共享t3这一个对象(共享资源).推荐(相对简单)
// 如果使用继承Thread的方式,那么每new一个Thread对象,每个对象的资源是自己独享的,如果需要共享资源那么就要加上static(成为类的属性或方法来实现共享),(但是这有点麻烦)
Thread thread01 = new Thread(t3);
Thread thread02 = new Thread(t3);
thread01.start();
thread02.start();

System.out.println("主线程完毕..");

image-20220121171947896.

继承Thread共享资源
T t1 = new T("hello ");// T类 继承了Thread类
T t2 = new T("hello ");// T类 继承了Thread类
t1.start();
t2.start();
System.out.println("主线程完毕..");

image-20220121172412073.

售票窗口 引起的> 类和对象的关系

// 自己写的售票窗口(存在很大的问题)

package com.lemon.ticket;

/**
 * @author 软柠柠吖
 * @date 2022/1/20
 */
public class SellTicket {
    public static void main(String[] args) {
        SellTicket1 s1 = new SellTicket1();
        SellTicket2 s2 = new SellTicket2();
        SellTicket3 s3 = new SellTicket3();
        s1.start();
        s2.start();
        s3.start();
    }
}
class SellTicket1 extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("销售一张票..剩余票数=" + Ticket.nums);
            Ticket.nums--;
            System.out.println();
            if (Ticket.nums == 0) {
                break;
            }
        }
    }
}
class SellTicket2 extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("销售一张票..剩余票数=" + Ticket.nums);
            Ticket.nums--;
            if (Ticket.nums == 0) {
                break;
            }
        }
    }
}
class SellTicket3 extends Thread {
    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("销售一张票..剩余票数=" + Ticket.nums);
            Ticket.nums--;
            if (Ticket.nums == 0) {
                break;
            }
        }
    }
}
class Ticket {
   static int nums = 10;
}

// 问题剖析
// 1.编码太死板,三个售票窗口就写了三个类,那有10个、100个窗口还要写10个、100个类吗?
//   这不显得编程太死板了.没有真正理解类和对象的关系
//  解决方案:我们编程要和实际生活联系在一起,学编程是来方便我们解决问题的。
//   实际生活中的售票窗口在java中可以当做一个个的窗口对象.
//	 我们编写Java程序时,需要完成的是编写售票窗口类,来简化程序,用new 的方式创建对象,即创建一个个的售票窗口.
//因此:我们在以后的编程时,把实际生活中的一个个事物当成一个个的对象,我们需要做的就是编写相应的类。
// 例如:要求我们创建3辆车,我们要做的是创建一个汽车类,然后new 出3辆车,而不是创建3个汽车类,在分别new对象.
// 三个售票窗口 ==> 售票窗口类 ==> new出三个售票窗口对象
// 三辆车 ==> 汽车类 ==> new出三辆车
// 实际问题 ==> 对应的类 ==> new出相应的对象

多线程售票问题(超卖)

继承Thread类
package com.lemon.ticket;


/**
 * @author 软柠柠吖
 * @date 2022/1/20
 * 使用多线程,模拟三个窗口同时售票 100张
 */
public class SellTicket {
    public static void main(String[] args) {
        // 测试
        SellTicket1 sellTicket1 = new SellTicket1();// 第1个线程-窗口
        SellTicket1 sellTicket2 = new SellTicket1();// 第2个线程-窗口
        SellTicket1 sellTicket3 = new SellTicket1();// 第3个线程-窗口

        // 这里我们会出现超卖..
        sellTicket1.start();// 启动售票线程
        sellTicket2.start();// 启动售票线程
        sellTicket3.start();// 启动售票线程
    }
}

// 使用Thread方式
class SellTicket1 extends Thread {
    private static int ticketNum = 10;// 让多个线程共享 ticketNum,这里需要static

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票完成..");
                break;
            }

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "销售一张票..剩余票数=" + (--ticketNum));
        }
    }
}
实现Runnable接口
package com.lemon.ticket;


/**
 * @author 软柠柠吖
 * @date 2022/1/20
 * 使用多线程,模拟三个窗口同时售票 100张
 */
public class SellTicket {
    public static void main(String[] args) {
     	// 也会出现超卖..
        SellTicket02 sellTicket02 = new SellTicket02();
        new Thread(sellTicket02).start();// 第1个线程-窗口
        new Thread(sellTicket02).start();// 第2个线程-窗口
        new Thread(sellTicket02).start();// 第3个线程-窗口
    }
}

// 实现接口方式
class SellTicket02 implements Runnable {
    private int ticketNum = 10;// 让多个线程共享 ticketNum,不需要static

    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票完成..");
                break;
            }

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "销售一张票..剩余票数=" + (--ticketNum));
        }
    }
}

线程终止-通知方式

基本说明(2点)

  1. 线程完成任务 后,会 自动退出
  2. 还可以通过 使用变量控制run方法退出 的方式停止线程,即通知方式

示例代码

// 通知线程退出
package com.lemon.exit_;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 * 演示-> 通知线程退出
 * 启动一个线程t,要求在 main线程 中去停止线程t
 */
public class ThreadExit_ {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();

        System.out.println("主线程休眠10s...");
        // 让主线程休眠10秒,再通知 t1线程退出
        Thread.sleep(10 * 1000);
        // 如果希望main线程去控制t1 线程的终止,必须可以修改 loop
        // 让t1 退出run方法,从而终止 t1线程 -> 通知方式
        t1.setLoop(false);
    }
}

class T extends Thread {
    private int count = 0;
    // 设置一个控制变量
    private boolean loop = true;

    @Override
    public void run() {
        while (loop) {

            try {
                // 让当前线程休眠50ms
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("T 运行中..." + (++count));
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

线程常用方法

常用方法第一组(8个)

Priority 优先级.

interrupt 中断.

// 1.setName // 设置线程名称,使之与参数 name 相同
// 2.getName // 返回该线程的名称
// 3.start // 使该线程开始执行;Java虚拟机底层调用该线程的start0 方法
// 4.run // 调用该线程对象的run方法
// 5.setPriority // 更改线程的优先级
// 6.getPriority // 获取线程的优先级
// 7.sleep // 在指定的 毫秒数 内让当前正在执行的 线程休眠(暂停执行)
// 8.interrupt // 中断线程,注意:interrupt不是终止(停止)线程,只是中断线程
// 9.currentThread()// 静态方法,获取当前的线程
//   Thread.currentThread().join();// 没有任何意义

第一组的注意事项和细节(4点)

  1. start 底层 会创建新的线程 start0(),调用run方法,run方法就是一个 简单的方法run方法 本身不会创建新线程.
  2. interrupt,中断线程,但并没有真正的结束线程。所以一般用于 中断正在休眠的线程.
  3. sleep:线程的静态方法,使当前线程休眠.
  4. Thread.currentThread(): 获取当前的线程,比如获取main方法所在的线程,这个线程既没有继承Thread 也没有实现Runnable接口,如果需要使用线程的相关方法,就可以使用currentThread()方法获取当前线程,然后再调用我们需要的相关的方法。
  5. 线程优先级的范围

image-20220122135235931.

示例代码

package com.lemon.method;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 */
public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        // 测试相关方法
        T t = new T();
        t.setName("软柠柠吖");
        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线程的休眠
        t.interrupt();
        System.out.println();
    }
}

class T extends Thread {// 自定义的线程类

    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 100; i++) {
                // Thread.currentThread().getName() 获取当前线程的名称
                System.out.println(Thread.currentThread().getName() + " 吃包子~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);// 20秒
            } catch (InterruptedException e) {
                // 当该线程执行到一个 interrupt方法时,就会catch 一个异常,可以加入自己的业务代码
                // InterruptedException e 是捕获到一个中断异常
                System.out.println(Thread.currentThread().getName() + " 被 interrupt了..");
            }
        }
    }
}

常用方法第二组(2个)

yield 让步.

  1. yield线程的礼让,静态方法。让出cpu,,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功。不能保证一定成功,cpu资源充足,则礼让很可能不成功.

  2. join线程的插队。插队的线程一旦插入成功,则肯定 先执行完 插入的线程的所有任务

    调用对方join方法,不是调用自己的join方法.

image-20220122142148822.

image-20220122142211892.

示例代码

package com.lemon.method;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 */
public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        T3 t3 = new T3();
        t3.start();

        for (int i = 1; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主线程(小弟)吃了 " + i + " 个包子..");
            if (i == 5) {
                System.out.println("主线程(小弟) 让 子线程(老大)先吃");
                // join:线程插队,一定会成功
                t3.join();// 这里相当于让t3 线程先执行完毕

                // Thread.yield();// 礼让,不一定成功
                System.out.println("主线程(小弟) 继续吃");
                //Thread.currentThread().join();// 没有任何意义,线程会阻塞
            }
        }

    }
}

class T3 extends Thread {// 自定义的线程类

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);// 1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程(老大)吃了 " + i + " 包子~~~");
        }
    }
}

线程插队练习

package com.lemon.method;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 */
public class ThreadMethodExercise {
    public static void main(String[] args) throws InterruptedException {
    
        //用了2个for循环,是不是有点麻烦啊...
        // 可以使用if来简化程序
    //    for (int i = 1; i <= 5; i++) {
    //        Thread.sleep(100);
    //        System.out.println("hi " + i);
    //    }
    //    T2 t2 = new T2();
    //    Thread thread = new Thread(t2);
    //    // thread.join();// 无用
    //    thread.start();
    //    thread.join();
    //    
    //    for (int i = 6; i <= 10; i++) {
    //        Thread.sleep(100);
    //        System.out.println("hi " + i);
    //    }
    //    System.out.println("主线程结束");
        T2 t2 = new T2();
        Thread thread = new Thread(t2);// 创建子线程
        for (int i = 1; i <= 10; i++) {
            System.out.println("hi " + i);

            if (i == 5) {// 说明主线程输出5次hi
                // thread.join();// 无用
                thread.start();// 启动子线程
                thread.join();// 立即将thread子线程,插入到main线程,让thread先执行
            }
            Thread.sleep(100);
        }// for
        
        System.out.println("主线程结束");
    }
}

class T2 implements Runnable {

    private int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("hello" + (++count));
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 10) {
                System.out.println("子线程结束...");
                break;
            }
        }
    }
}

用户线程和守护线程(3点)

基本介绍

Daemon 守护线程.

  1. 用户线程:也叫工作线程,当线程的任务执行完成,或者以通知方式结束
  2. 守护线程:一般是 为工作线程服务 的,当所有的用户线程结束,守护线程自动结束
  3. 常见的守护线程:垃圾回收机制

示例代码

package com.lemon.method;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 */
public class  ThreadMethod03 {
    public static void main(String[] args) throws InterruptedException {
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        
        // 如果我们希望当main线程结束后,子线程自动结束
        // 只需将子线程设置成 守护线程即可
        myDaemonThread.setDaemon(true);
        // 先设置守护线程,再启动线程
        myDaemonThread.start();
        for (int i = 1; i <= 10; i++) {
            System.out.println("宝强辛苦的工作...");
            Thread.sleep(1000);
        }
    }
}
class MyDaemonThread extends Thread {
    @Override
    public void run() {
        for (;;) {// 无限循环
            try {
                Thread.sleep(1000);// 休眠1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("马蓉和宋喆快乐聊天,哈哈哈...");
        }
    }
}

线程的生命周期

基本介绍

  • JDK中用 Thread.State 枚举表示了线程的几种状态

image-20220122172701385..

线程状态转换图

image-20220122172701385.

线程七大状态

  1. New.
  2. Runnable-Ready.
  3. Runnable-Running.
  4. Timed_Waiting.
  5. Waiting.
  6. Blocked.
  7. Terminated.

写程序查看线程状态

State 状态.

package com.lemon.state_;

/**
 * @author 软柠柠吖
 * @date 2022/1/21
 */
public class ThreadState_ {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        System.out.println(t.getName() + " 状态 " + t.getState());
        t.start();

        while (Thread.State.TERMINATED != t.getState()) {
            System.out.println(t.getName() + " 状态 " + t.getState());
            Thread.sleep(500);
        }
        System.out.println(t.getName() + " 状态 " + t.getState());
    }
}

class T extends Thread {
    @Override
    public void run() {

        for (int i = 0; i < 10; i++) {
            System.out.println("hi " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

线程的同步

线程同步机制(2点)

  1. 在多线程编程,一些 敏感数据 不允许被多个线程 同时访问,此时就使用同步访问技术,保证数据在任何同一时刻最多有一个线程访问,以保证数据的完整性.
  2. 也可以这样理解:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作其他线程才能对该内存地址进行操作

同步具体方法-Synchronized

  1. 同步代码块.

    synchronized(对象) {// 得到对象的锁,才能操作同步代码
        // 需要被同步的代码
    }
    
  2. synchronized 还可以放在方法声明中,表示整个方法-为同步方法.

    public synchronized void m(String name) {
        // 需要被同步的代码
    }
    
  3. 如何理解:

    就好像 某小伙伴上厕所前先把门关上(上锁),完事后再出来(解锁),那么其他小伙伴就可在使用厕所了.

售票问题解决

class SellTicket03 implements Runnable {
    private int ticketNum = 100;// 让多个线程共享 ticketNum
    private boolean loop = true;// 控制run方法变量

    // 解读
    // 1.这里使用了synchronized 实现了线程同步
    // 2.当多个线程执行到这里时,就会去争夺 this对象锁
    // 3.哪个线程争夺到(获取) this对象锁,就执行 synchronized方法,执行完后,会释放 this对象锁
    // 4.争夺不到 this对象锁的线程,就处于Blocked状态,准备继续争夺
    // 5.this对象锁 是非公平锁
    public synchronized void sell() {// 同步方法,在同一时刻,只能有一个线程来执行run方法
        if (ticketNum <= 0) {
            System.out.println("售票完成..");
            loop = false;
            return;

        }

        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "销售一张票..剩余票数=" + (--ticketNum));
    }

    @Override
    public void run() {
        while (loop) {
            sell();// sell方法是一个同步方法
        }
    }
}

分析同步原理

image-20220125083924354.

互斥锁-synchronized

基本介绍(6)

  1. Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
  2. 每个对象都对应于一个可称为 "互斥锁" 的标记,这个标记用来保证在任意同一时刻,只能有一个线程访问该对象
  3. 关键字synchronized 来与对象的互斥锁联系。当某个对象用 synchronized 修饰时,表明该对象在任何一个时刻只能由一个线程访问
  4. 同步的局限性:导致程序的执行效率要降低
  5. 同步方法(非静态的) 的锁可以是this,也可以是其他对象(要求是同一个对象。如果不是同一对象,则锁根本就没有发挥作用,还是会出现问题,例如售票问题)
  6. 同步方法(静态的) 的锁为当前类本身类.class.

synchronized原理机制图

image-20220123151821025.

示例代码

继承Thread-实现锁
// 使用Thread方式
// new SellTicket04().start();
// new SellTicket04().start();
class SellTicket04 extends Thread {
    public synchronized void m1() {// 根本锁不住,不是同一个对象
        System.out.println("hello");
    }
    public void m2() {
        synchronized (this) {// 等价于上面,也根本锁不住
            System.out.println("hello");
        }
    }
    public void m3() {
        synchronized (SellTicket04.class) {// 类对象,可以锁住
            System.out.println("hello");
        }
    }
    static Object object = new Object();
    public void m4() {
        synchronized (object) {// 同一对象,静态对象,可以锁住
            System.out.println("hello");
        }
    }
}
实现Runnable接口-实现锁
// 实现接口方式
// SellTicket03 sellTicket03 = new SellTicket03();
// new Thread(sellTicket03).start();// 第1个线程-窗口
// new Thread(sellTicket03).start();// 第2个线程-窗口
// new Thread(sellTicket03).start();// 第3个线程-窗口
class SellTicket03 implements Runnable {
    private int ticketNum = 100;// 让多个线程共享 ticketNum
    private boolean loop = true;// 控制run方法变量
    Object object = new Object();// 同一个对象

    // 同步方法(静态的) 的锁为当前类本身
    // 解读
    // 1.public synchronized static void m1() {} 锁是加在 SellTicket03.class
    // 2.如果在静态方法中,实现一个同步代码块
    //      public static void m2() {
    //          synchronized (SellTicket03.class) {
    //              System.out.println("m2");
    //          }
    //      }

    public synchronized static void m1() {

    }

    public static void m2() {
        // 静态方法,加this->报错,只能用类.class
        // 类加载时,还没有对象的创建,所有不能用this
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
    }

    // 说明
    // 1.public synchronized void sell() {} 就是一个同步方法
    // 2.这时锁在 this对象
    // 3.也可以在代码块上写 synchronized,同步代码块, 互斥锁还是加在 this对象
    public /*synchronized*/ void sell() {// 同步方法,在同一时刻,只能有一个线程来执行run方法
        // object = new Object();// 这是不同对象
        synchronized (object/*new Object()*/) {
            if (ticketNum <= 0) {
                System.out.println("售票完成..");
                loop = false;
                return;


            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "销售一张票..剩余票数=" + (--ticketNum));
        }
    }

    @Override
    public void run() {
        while (loop) {
            sell();// sell方法是一个同步方法
        }
    }
}

注意事项和细节(3)

  1. 同步方法如果没有使用static修饰:默认锁对象为this.
  2. 同步方法如果使用static修饰,默认锁对象:当前类.class
  3. 实现互斥锁的步骤:
    • 需要先分析上锁的代码
    • 选择同步代码块【推荐】或同步方法
    • 要求多个线程的锁对象为同一个即可!

线程的死锁

基本介绍

  • 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程时一定要避免死锁的发生。

生活案例

  • 妈妈:你先完成作业,才让你玩手机。
  • 小明:你先让我玩手机,我才完成作业。

示例代码

package com.lemon.syn;

/**
 * @author 软柠柠吖
 * @date 2022/1/23
 */
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
    static Object o2 = new Object();
    boolean flag;

    public DeadLockDemo(boolean flag) {// 构造器
        this.flag = flag;
    }

    @Override
    public void run() {

        // 下面业务逻辑的分析
        // 1.如果flag 为true,线程A 就会先得到/持有 o1 对象锁,然后尝试去获取 o2 对象锁
        // 2.如果线程A 得不到 o2 对象锁,就会一直处于 Blocked 状态
        // 3.如果flag 为false,线程B 就会先得到/持有 o2对象锁,然后尝试去获取 o1 对象锁
        // 4.如果线程 得不到 oB 对象锁,就会一直处于 Blocked 状态
        // 5.这样就容易出现 死锁现象
        if (flag) {
            synchronized (o1) {// 对象互斥锁,下面就是同步代码
                System.out.println(Thread.currentThread().getName() + " 进入1");
                synchronized (o2) {//
                    System.out.println(Thread.currentThread().getName() + " 进入2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + " 进入3");
                synchronized (o1) {//
                    System.out.println(Thread.currentThread().getName() + " 进入4");
                }
            }
        }
    }
}

释放锁

释放锁操(4点)

  1. 当前线程的同步方法、同步代码块执行结束
    • 上厕所,完事出来。
  2. 当前线程在同步代码块、同步方法中遇到break、return.
    • 没有正常的完事,经理叫他修该bug,不得不出来。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception导致异常结束.
    • 没有正常的完事,发现忘带纸,不得已出来。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法当前线程暂停并释放锁.
    • 没有正常完事,觉得需要酝酿下,所以出来等会再进去。

不释放锁操作

suspend 挂起.

  1. 线程执行同步代码块或同步方法时,程序调用了 Thread.sleep()Thread.yield()方法,暂停当前线程的执行,不会释放锁

    • 上厕所,太困了,在坑位上眯一会。
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法,将该线程挂起,该线程不会是否锁。

    提示:应尽量避免使用suspend() 和 resume() 来控制线程,方法不在推荐使用。

坦克大战2.0

发射子弹思路

  • 添加功能:当玩家按一下 j 键,就会发射一颗子弹。
  1. 当发射一颗子弹后,就相当于启动一个线程。
  2. 我们的 Hero 类 有子弹的对象,当按下 j 时,我们就启动一个发射的行为(线程),让子弹不停的移动,形成一个射击的效果。
  3. 我们 MyPanel 需要不停的重绘子弹,才能出现该效果。
  4. 当子弹移动到 面板的边界时,就应该销毁(把启动的子弹的线程销毁)。

示例代码

ConcurrentModificationException(并发修改异常).

Bomb类
package com.lemon.tankgame3;

/**
 * @author 软柠柠吖
 * @date 2022/1/28
 *炸弹类
 */
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;
        }
    }
}
Enemy类
package com.lemon.tankgame3;

import java.util.Vector;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 *敌人坦克类
 */
public class Enemy extends Tank implements Runnable {

    // 这里count 和 speed 并非等价,因为一直在不断的重绘
    // for循环 中每调用一次移动的方法,坦克就移动一次
    // 而 修改speed 是直接修改 移动方法,会出现坦克瞬移的现象
    int count = 15;

    public Enemy(int x, int y, int direct, int type, Wall wall) {
        super(x, y, direct, type, wall);

        // 启动敌方坦克线程
        new Thread(this).start();
    }

    @Override
    public void run() {
        int n = 0;
        while (true) {
            // 写并发程序,一定要考虑清楚,该线程什么时候结束
            if (!isLive()) {
                break;
            }

            // 休眠800毫秒
            try {
                Thread.sleep(800);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 随机改变坦克方向
            n = (int) (Math.random() * 100 + 1);
            if (n > 75) {
                setDirect(0);
                for (int i = 0; i < count; i++) {
                    if (getY() - 30 > 0) {
                        moveUp();
                    }
                }
            } else if (n > 50) {
                setDirect(1);
                for (int i = 0; i < count; i++) {
                    if (getX() + 20 < wall.x) {
                        moveRight();
                    }
                }
            } else if (n > 25) {
                setDirect(2);
                for (int i = 0; i < count; i++) {
                    if (getY() + 30 < wall.y) {
                        moveDown();
                    }
                }
            } else {
                setDirect(3);
                for (int i = 0; i < count; i++) {
                    if (getX() - 20 > 0) {
                        moveLeft();
                    }
                }
            }
            //setDirect((getDirect()+4+1)%4);

            if (shots.size() < 1) {
                shotTank();
            }

        } // while
    }
}
Hero类
package com.lemon.tankgame3;


/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class Hero extends Tank {

    public Hero(int x, int y, int direct, int type, Wall wall) {
        super(x, y, direct, type, wall);
    }
}
MyPanel类
package com.lemon.tankgame3;

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Iterator;
import java.util.Vector;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 * 坦克大战的绘图区域
 */
// 为了监听 键盘事件,我们要实现KeyListener
// 为了让Panel 不停的重绘子弹,需要将MyPanel 实现Runnable,当做一个线程使用
public class MyPanel extends JPanel implements KeyListener, Runnable {
    // 定义我的坦克
    Hero hero = null;

    // 敌人坦克组
    // 考虑多线程问题 用集合Vector
    Vector<Enemy> enemies = new Vector<>();
    // Enemy[] enemies = new Enemy[3];
    int enemyTankSize = 3;

    // 墙体
    Wall wall = null;

    // 定义一个Vector,用于存放炸弹
    // 说明:当子弹击中坦克时,加入一个Bomb对象到bombs
    Vector<Bomb> bombs = new Vector<>();

    // 定义三张炸弹图片,用于显示爆炸效果
    Image image1 = null;
    Image image2 = null;
    Image image3 = null;


    public MyPanel() {
        // 墙体
        wall = new Wall(1000, 750);

        // 初始化自己的坦克
        hero = new Hero(500, 450, 0, 0, wall);
        hero.setSpeed(9);

        // 初始化敌方坦克
        for (int i = 0; i < enemyTankSize; i++) {
            enemies.add(new Enemy(100 * (i + 1), 30, 2, 1, wall));
        }

        // 初始化图片对象
        image1 = Toolkit.getDefaultToolkit().getImage(MyPanel.class.getResource("/bomb_1.gif"));
        image2 = Toolkit.getDefaultToolkit().getImage(MyPanel.class.getResource("/bomb_2.gif"));
        image3 = Toolkit.getDefaultToolkit().getImage(MyPanel.class.getResource("/bomb_3.gif"));

    }


    @Override
    public void paint(Graphics g) {
        super.paint(g);
        // 游戏区域背景(默认为黑色)
        g.fillRect(0, 0, 1000, 750);


        // 画出坦克-封装方法
        // drawTank(hero.getX(), hero.getY(), g, hero.getDirect(), hero.getType());
        if (hero.isLive()) {
            //activeArea(hero, wall);

            madeTank(hero, g);
        }

        // 如果bombs 集合中有对象,就画出
        for (int i = 0; i < bombs.size(); i++) {
            // 取出炸弹
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Bomb bomb = bombs.get(i);
            // 根据当前这个bomb对象的life值去画出对应的图片
            if (bomb.life > 6) {
                g.drawImage(image1, bomb.x - 30, bomb.y - 30, 60, 60, this);
            } else if (bomb.life > 3) {
                g.drawImage(image2, bomb.x - 30, bomb.y - 30, 60, 60, this);
            } else {
                g.drawImage(image3, bomb.x - 30, bomb.y - 30, 60, 60, this);
            }
            // 让这个炸弹的生命值减少
            bomb.lifeDown();
            // 如果bomb life 为0, 就从bombs 的集合中删除
            if (bomb.life == 0) {
                bombs.remove(bomb);
                i--;
            }
        }


        // 遍历绘制敌方坦克
        for (Enemy enemy : enemies) {
            if (enemy.isLive()) {
                // activeArea(enemy, wall);
                madeTank(enemy, g);
            }

            // 遍历绘制子弹
            for (int i = 0; i < enemy.shots.size(); i++) {
                // 取出子弹
                Shot shot = enemy.shots.get(i);
                // 绘制
                if (shot.isLive) {
                    madeBullet(shot, g);
                } else {
                    enemy.shots.remove(shot);
                    i--;
                }
            }

        }

        // 遍历绘制我方坦克子弹
        for (int i = 0; i < hero.shots.size(); i++) {
            Shot shot = hero.shots.get(i);
            if (shot != null && shot.isLive) {
                madeBullet(shot, g);
            } else {
                hero.shots.remove(shot);
            }
        }
    }

    // 绘制坦克
    public void madeTank(Tank tank, Graphics g) {
        drawTank(tank.getX(), tank.getY(), g, tank.getDirect(), tank.getType());
    }

    // 绘制子弹
    public void madeBullet(Shot shot, Graphics g) {
        drawBullet(shot.x, shot.y, g, shot.direct, shot.type);
    }
	// 坦克的活动区域
    public void activeArea(Tank tank, Wall wall) {
        switch (tank.getDirect()) {
            case 0:// 坦克向上
            case 2:// 坦克向下
                if (tank.getX() - 20 < 0) {// 左
                    tank.left = false;
                } else if (tank.getX() + 20 >= wall.x) {// 右
                    tank.right = false;
                }

                if (tank.getY() - 30 < 0) {// 上
                    tank.up = false;
                } else if (tank.getY() + 30 >= wall.y) {// 下
                    tank.down = false;
                }

                break;
            case 1:// 坦克向右
            case 3:// 坦克向左
                if (tank.getX() - 30 < 0) {
                    tank.left = false;
                } else if (tank.getX() + 30 >= wall.x) {
                    tank.right = false;
                }

                if (tank.getY() - 20 < 0) {
                    tank.up = false;
                } else if (tank.getY() + 20 >= wall.y) {
                    tank.down = false;
                }

                break;
            default:
                System.out.println("方向有误");
                break;
        }
    }

    /**
     * 编写方法,画出坦克
     *
     * @param x      坦克左上角x坐标
     * @param y      坦克左上角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;
            default:
                g.setColor(Color.pink);
                break;
        }

        // 根据坦克方向,来绘制对应形状的坦克
        switch (direct) {
            case 0:// 方向朝上
                // 坦克左轮
                g.fill3DRect(x - 20, y - 30, 10, 60, false);
                // 坦克右轮
                g.fill3DRect(x + 10, y - 30, 10, 60, false);
                // 坦克中间
                g.fill3DRect(x - 10, y - 20, 20, 40, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x, y - 10, x, y - 35);
                break;
            case 1:// 方向朝右
                // 坦克左轮
                g.fill3DRect(x - 30, y - 20, 60, 10, false);
                // 坦克右轮
                g.fill3DRect(x - 30, y + 10, 60, 10, false);
                // 坦克中间
                g.fill3DRect(x - 20, y - 10, 40, 20, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x + 10, y, x + 35, y);
                break;
            case 2:// 方向朝下
                // 坦克左轮
                g.fill3DRect(x - 20, y - 30, 10, 60, false);
                // 坦克右轮
                g.fill3DRect(x + 10, y - 30, 10, 60, false);
                // 坦克中间
                g.fill3DRect(x - 10, y - 20, 20, 40, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x, y + 10, x, y + 35);
                break;
            case 3:// 方向朝左
                // 坦克左轮
                g.fill3DRect(x - 30, y - 20, 60, 10, false);
                // 坦克右轮
                g.fill3DRect(x - 30, y + 10, 60, 10, false);
                // 坦克中间
                g.fill3DRect(x - 20, y - 10, 40, 20, false);
                // 坦克盖子
                g.fillOval(x - 10, y - 10, 20, 20);
                // 炮管
                g.drawLine(x - 10, y, x - 35, y);
                break;
            default:
                break;
        }
    }

    /**
     * 子弹的绘制
     *
     * @param x      子弹横坐标
     * @param y      子弹纵坐标
     * @param g      画笔
     * @param direct 子弹方向
     * @param type   子弹类型
     */
    public void drawBullet(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;
            default:
                g.setColor(Color.pink);
                break;
        }

        // 根据坦克方向,来绘制对应形状的子弹
        switch (direct) {
            case 0:// 方向朝上
                g.fillRect(x, y, 4, 4);
                break;
            case 1:// 方向朝右
                g.fillRect(x, y, 4, 4);
                break;
            case 2:// 方向朝下
                g.fillRect(x, y, 4, 4);
                break;
            case 3:// 方向朝左
                g.fillRect(x, y, 4, 4);
                break;
            default:
                break;
        }
    }

    // 编写方法,判断我方子弹是否击中敌方坦克
    public void hitTank(Shot shot, Tank tank) {
//        if (!shot.isLive || !tank.isLive()) {
//            return;
//        }
        // 判断shot 击中坦克
        // 这里用了switch 的穿透!!
        switch (tank.getDirect()) {
            case 0:// 坦克向上
            case 2:// 坦克向下
                if (shot.x > tank.getX() - 20 && shot.x < tank.getX() + 20
                        && shot.y > tank.getY() - 30 && shot.y < tank.getY() + 30) {
                    shot.isLive = false;
                    tank.setLive(false);
                    // 当我的子弹击中敌人坦克后,将tank 从集合Vector 中拿掉
                    enemies.remove(tank);
                    // 创建Bomb对象,加入到bombs集合
                    Bomb bomb = new Bomb(tank.getX(), tank.getY());
                    bombs.add(bomb);
                }
                break;
            case 1:// 坦克向右
            case 3:// 坦克向左
                if (shot.x > tank.getX() - 30 && shot.x < tank.getX() + 30
                        && shot.y > tank.getY() - 20 && shot.y < tank.getY() + 20) {
                    shot.isLive = false;
                    tank.setLive(false);
                    enemies.remove(tank);
                    // 创建Bomb对象,加入到bombs集合
                    Bomb bomb = new Bomb(tank.getX(), tank.getY());
                    bombs.add(bomb);
                }
                break;
            default:
                System.out.println("方向有误");
                break;
        }

    }


    @Override
    public void keyTyped(KeyEvent e) {

    }

    // 处理wasd 键按下的情况
    @Override
    public void keyPressed(KeyEvent e) {
//        if (e.getKeyCode() == KeyEvent.VK_S) {// 按下S键 下
//            if (hero.getY() + 30 < wall.y) {
//                hero.setDirect(2);// 改变坦克的方向
//                hero.moveDown();// 坦克向下移动
//            }
//        } else if (e.getKeyCode() == KeyEvent.VK_W) {// 按下W键 上
//            if (hero.getY() - 30 > 0) {
//                hero.setDirect(0);
//                hero.moveUp();
//            }
//        } else if (e.getKeyCode() == KeyEvent.VK_A) {// 按下A键 左
//            if (hero.getX() - 20 > 0) {
//                hero.setDirect(3);
//                hero.moveLeft();
//            }
//        } else if (e.getKeyCode() == KeyEvent.VK_D) {// D键 右
//            if (hero.getX() + 20 < wall.x) {
//                hero.setDirect(1);
//                hero.moveRight();
//            }
//        }
        activeArea(hero, wall);
        if (e.getKeyCode() == KeyEvent.VK_S) {// 按下S键 下
            if (hero.down) {
                hero.setDirect(2);// 改变坦克的方向
                hero.moveDown();// 坦克向下移动
                hero.up = true;
            }
        } else if (e.getKeyCode() == KeyEvent.VK_W) {// 按下W键 上
            if (hero.up) {
                hero.setDirect(0);
                hero.moveUp();
                hero.down = true;
            }
        } else if (e.getKeyCode() == KeyEvent.VK_A) {// 按下A键 左
            if (hero.left) {
                hero.setDirect(3);
                hero.moveLeft();
                hero.right = true;
            }
        } else if (e.getKeyCode() == KeyEvent.VK_D) {// D键 右
            if (hero.right) {
                hero.setDirect(1);
                hero.moveRight();
                hero.left = true;
            }
        }

        // 如果用户按下的是J,就发射
        if (e.getKeyCode() == KeyEvent.VK_J) {
            System.out.println("发射子弹..");
//            hero.shot·EnemyTank();
            hero.shotTank();


        }

        // 让面板重绘
        repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {

    }

    @Override
    public void run() {// 每隔500ms,重绘区域,刷新绘图区域,子弹就移动
        while (true) {
            // 判断是否有敌方坦克被击中
            for (Shot shot : hero.shots) {
                for (int i = 0; i < enemies.size(); i++) {
                    hitTank(shot, enemies.get(i));
                }
//                for (Enemy enemy : enemies) {
//                    hitTank(shot, enemy);
//                }
            }

            // 判断我方坦克是否被击中
            for (int i = 0; i < enemies.size(); i++) {
                for (int j = 0; j < enemies.get(i).shots.size(); j++) {
                    Shot shot = enemies.get(i).shots.get(j);
                    hitTank(shot, hero);
                }
            }
//            for (Enemy enemy : enemies) {
//                for (int i = 0; i < enemy.shots.size(); i++) {
//                    Shot shot = enemy.shots.get(i);
//                    hitTank(shot, hero);
//                }
//            }
            // 重绘
            repaint();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
Shot类
package com.lemon.tankgame3;

/**
 * @author 软柠柠吖
 * @date 2022/1/25
 */
public class Shot implements Runnable {
    int x;// 子弹x坐标
    int y;// 子弹y坐标
    int direct = 0;// 子弹方向
    int speed = 2;// 子弹速度
    int type;
    boolean isLive = true;// 子弹是否存活
    Wall wall = null;

    // 构造器
    public Shot(int x, int y, int direct, int type, Wall wall) {
        this.x = x;
        this.y = y;
        this.direct = direct;
        this.type = type;
        this.wall = wall;
    }

    @Override
    public void run() {// 射击
        while (true) {
            // 线程休眠 50ms
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 根据方法来改变 x,y坐标
            switch (direct) {
                case 0:// 方向朝上
                    y -= speed;
                    break;
                case 1:// 方向朝右
                    x += speed;
                    break;
                case 2:// 方向朝下
                    y += speed;
                    break;
                case 3:// 方向朝左
                    x -= speed;
                    break;
                default:
                    break;
            }
            //System.out.println("子弹 x=" + x + " y=" + y);

            if (!(isLive && x >= 0 && x <= wall.x && y >= 0 && y <= wall.y)) {
                isLive = false;
                break;
            }
        }
    }
}
Tank类
package com.lemon.tankgame3;

import java.util.Vector;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class Tank {// 坦克父类
    private int x;// 坦克的横坐标
    private int y;// 坦克的纵坐标
    private int direct;// 坦克方向 0上 1右 2下 3左
    private int type = 0;// 坦克类型
    private int speed = 1;// 坦克速度
    private boolean isLive = true;// 是否存活
    boolean up = true;// 向上移动
    boolean right = true;
    boolean down = true;
    boolean left = true;
    int isActive = -1;// 是否移动
    Wall wall = null;
    Vector<Shot> shots = new Vector<>(100);


    public Tank(int x, int y, int direct, int type, Wall wall) {
        this.x = x;
        this.y = y;
        this.direct = direct;
        this.type = type;
        this.wall = wall;
    }

    // 射击
    public void shotTank() {
        if (!isLive) {
            return;
        }
        Shot shot = null;

        // 创建Shot对象, 根据当前Hero对象的位置和方向来创建Shot
        switch (getDirect()) {
            case 0:// 方向朝上
                shot = new Shot(getX() - 1, getY() - 37, 0, getType(), wall);
                break;
            case 1:// 方向朝右
                shot = new Shot(getX() + 37, getY() - 1, 1, getType(), wall);
                break;
            case 2:// 方向朝下
                shot = new Shot(getX() - 1, getY() + 37, 2, getType(), wall);
                break;
            case 3:// 方向朝左
                shot = new Shot(getX() - 37, getY() - 1, 3, getType(), wall);
                break;
            default:
                break;
        }
        // 启动我们的Shot线程
        shots.add(shot);
        new Thread(shot).start();
    }

    // 上右下左移动的方法
    public void moveUp() {

        y -= speed;
    }


    public void moveRight() {
        x += speed;
    }

    public void moveDown() {
        y += speed;
    }

    public void moveLeft() {
        x -= speed;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getDirect() {
        return direct;
    }

    public void setDirect(int direct) {
        this.direct = direct;
    }

    public int getSpeed() {
        return speed;
    }

    public int getType() {
        return type;
    }

    public void setType(int type) {
        this.type = type;
    }

    public boolean isLive() {
        return isLive;
    }

    public void setLive(boolean live) {
        isLive = live;
    }

    public void setSpeed(int speed) {
        if (speed <= 0 || speed > 10) {
            System.out.println("速度范围在1~10之间,默认为1");
            return;
        }
        this.speed = speed;
    }
}
Wall类
package com.lemon.tankgame3;

/**
 * @author 软柠柠吖
 * @date 2022/1/28
 * wall 墙
 */
public class Wall {
    int x;
    int y;

    public Wall(int x, int y) {
        this.x = x;
        this.y = y;
    }
}
LemonTankGame03类
package com.lemon.tankgame3;

import javax.swing.*;

/**
 * @author 软柠柠吖
 * @date 2022/1/19
 */
public class LemonTankGame03 extends JFrame {
    private MyPanel myPanel = null;

    public static void main(String[] args) {
        new LemonTankGame03();
    }

    public LemonTankGame03() {
        myPanel = new MyPanel();
        Thread thread = new Thread(myPanel);
        thread.start();
        this.add(myPanel);// 把面板(就是游戏的绘图区域)
        this.addKeyListener(myPanel);// 让JFrame 监听myPanel的键盘事件
        this.setSize(1020, 800);
        this.setLocation(200, 200);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }
}

IO流

文件

什么是文件

  • 文件,对我们并不陌生,文件是保存数据的地方,比如大家经常使用的word文档、txt文件、excel文件...都是文件。它既可以保存一张图片,也可以保存视频,声音...

文件流

  • 文件在程序中是以 流 的形式 来操作的.
  • 针对内存定义输入流、输出流. 这一点及其重要,贯穿始终!!

image-20220201214321802.

  • 流:数据在数据源(文件) 和 程序(内存) 之间经历的路径.
  • 输入流:数据从数据源(文件) 程序(内存) 的路径.
  • 输出流:数据从程序(内存) 到 数据源(文件) 的路径.

常用的文件操作

创建文件

创建文件对象相关构造器和方法

  • 一个File对象 就代表一个文件
// 在java程序(内存) 中,创建file(文件)对象
new File(String pathName); // 方式1:根据路径构建一个File对象
new File(File parent, String child); // 方式2:根据父目录文件+子路径创建
new File(String parent, String child); // 方式3:根据父目录+子路径创建

// 在磁盘 中,创建真正的文件
createNewFile(); // 创建新文件

File类示例图

image-20220202105528420.

image-20220202105636572.

创建文件示例代码

package com.lemon.file;

import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;

/**
 * @author 软柠柠吖
 * @date 2022/2/2
 * 演示创建文件 3种方式
 */
public class FileCreate {
    // 方式1:new File(String pathName) // 根据路径构建一个File对象
    @Test
    public void fileCreate01() {
        String filePath = "D:\\lemon.txt";
        File file = new File(filePath);
        try {
            // 快捷键 Atl+T
            file.createNewFile();
            System.out.println("文件创建成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    // 方式2:new File(File parent, String child) // 根据父目录文件+子路径创建
    // D:\\lemon2.txt
    @Test
    public void fileCreate02() {
        File parentFile = new File("D:\\");
        String childName = "lemon2.txt";
        // 注意
        // 这里的file对象,在java程序中,只是一个对象,只是在内存中创建了文件对象 (内存)
        // 只有执行了 createNewFile 方法,才会真正的,在磁盘创建该文件 (磁盘)
        File file = new File(parentFile, childName);
        try {
            // 真正在磁盘中创建一个文件
            file.createNewFile();
            System.out.println("文件创建成功");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 方式3:new File(String parent, String child) // 根据父目录+子路径创建
    @Test
    public void fileCreate03() {
        // String parentPath = "D:/";
        String parentPath = "D:\\";// 推荐
        String childPath = "lemon3.txt";
        File file = new File(parentPath, childPath);

        try {
            file.createNewFile();
            System.out.println("创建成功~");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

获取文件的相关信息

  • getNamegetAbsolutePathgetParentlengthexistsisFileisDirectory.

示例代码

public class FileInformation {
    // 获取文件信息
    @Test
    public void fileInfo() {
        // 先在内存中创建文件对象
        File file = new File("D:\\lemon.txt");

        try {
            // 在磁盘中创建真正的文件
            file.createNewFile();
            System.out.println("文件创建~");
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 获取文件名称
        System.out.println("文件名=" + file.getName());// lemon.txt
        // 获取绝对路径
        System.out.println("绝对路径=" + file.getAbsolutePath());// D:\lemon.txt
        // 获取父级目录
        System.out.println("文件父级目录=" + file.getParent());// D:\\
        // 获取文件大小,单位: 字节
        System.out.println("文件大小(字节)=" + file.length());// 0
        // 文件是否存在
        System.out.println("文件是否存在=" + file.exists());// T
        // 是不是一个文件
        System.out.println("是不是一个文件=" + file.isFile());// T
        // 是不是一个目录
        System.out.println("是不是一个目录=" + file.isDirectory());// F
    }
}

目录操作和文件的删除

  • mkdir 创建一级目录、mkdirs 创建多级目录、delete 删除空目录或文件

示例代码

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

    }

    // 判断 D:\\lemon.txt 是否存在,存在就删除,否则提示不存在
    @Test
    public void m1() {
        // 创建file对象
        File file = new File("D:\\lemon.txt");
        // 判断file对象是否在磁盘中存在, 而非判断是否在java程序(内存)中存在。
        // 如果判断是否在java程序(内存)中存在,则结果一定为true,
        // 因为一开始就在内存中创建了一个file对象,毫无意义,所以是判断是否在磁盘中存在
        if (file.exists()) {
            if (file.delete()) {
                System.out.println("删除成功");
            } else {
                System.out.println("删除失败");
            }
        } else {
            System.out.println(file.getName() + " 该文件不存在");
        }
    }

    // 判断 D:\\demo02 是否存在,存在就删除,否则提示不存在
    // 这里我们需要体会到,在java编程中,目录(文件夹)也被当做文件
    @Test
    public void m2() {
        // 创建file对象
        File file = new File("D:\\demo02");
        if (file.exists()) {
            if (file.delete()) {
                System.out.println("删除成功");
            } else {
                System.out.println("删除失败");
            }
        } else {
            System.out.println(file.getName() + " 该文件夹(目录)不存在");
            file.mkdirs();

        }
    }

    // 判断 D:\\demo\\a\\b\\c 目录是否存在,如果存在就提示已经存在,否则就创建
    @Test
    public void m3() {
        // 创建file对象
        File file = new File("D:\\demo\\a\\b\\c");
        if (file.exists()) {
            System.out.println(file.getName() + " 该目录已存在");
        } else {
            // 创建一级目录使用: mkdir()
            // 创建多级目录使用: mkdirs()
            if (file.mkdirs()) {
                System.out.println("目录创建成功");
            } else {
                System.out.println("目录创建失败");
            }
        }
    }
}

IO流原理以及流的分类

输入流VS输出流

  • 输入流和输出流**怎么区分 ** ?
  1. Java 里的输入流与输出流是针对内存而言的,它是从内存中读写,而不是所说的显示与存盘。

  2. 输入流与输出流都可以将内容从屏幕上显示出来。

  3. 程序操作的数据都应该是在内存里面内存是操作数据的主对象。

  4. 把数据从其他资源中传送到内存,就是输入。反之,把数据从内存传送到其他资源,就是输出。

/*
  输入:其它数据源 --数据--> 内存(读到内存中) InputStream
  输出:内存 --数据--> 其它数据源(写出内存外) OutputSt
*/
  • 不管从磁盘、网络还是键盘读,读到内存中就是 InputStream。例如:
BufferedReader in = 
    new BufferedReader(new InputStreamReader(new FileInputStream("infilename")));
  • 不管写到磁盘、网络,或者写到屏幕,都是使用 OutputStream。例如:
BufferedWriter out = 
    new BufferedWriter(new OutputStreamWriter(new FileOutputStream("outfilename")));

输入输出的方向是针对程序而言,向程序(内存)中读入数据,就是输入流;从程序中向外写出数据,就是输出流。

java IO流原理(5点)

  1. I/OInput/Output的缩写,I/O技术是非常实用的技术, 用于处理数据传输。如读/写文件,网络通讯等。
  2. Java程序中,对于数据的输入/输出操作以 "(Stream)" 的方式进行。
  3. java.io 包下提供了各种 "流" 类 和 接口,用于获取不同种类的数据,并通过方法输入或输出数据.
  4. 输入input:读取外部数据(磁盘、光盘等存储设备的数据) 到 程序(内存)中。
  5. 输出output:将 程序(内存) 数据 输出到磁盘、光盘等存储设备中。

流的分类

(1)按操作数据单位:字节流、字符流

字节流(8bit):二进制文件(声音文件,视频文件等),无损操作

字符流(按字符):文本文件.

(2)按数据流的流向:输入流、输出流
(3)按流的角色:节点流、处理流(包装流)

字节流、字符流简单介绍

(抽象基类) 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer
  1. JavaIO 流共涉及40多个类,实际上非常规则,都是从如上4抽象基类派生的。

  2. 由这四个类派生出来的子类名称都是以其父类名作为子类名后缀.

  3. inputStream、outputStream、Reader、Writer 都是抽象类(abstract).

流 VS 文件

image-20220206101223474.

字节流说明

InputStream:字节输入流

基本介绍

  • InputStream 抽象类是所有类 字节输入流的超类.
  • InputStream 常用的子类:
    1. FileInputStream:文件字节输入流.
    2. BufferedInputStream:缓存字节输入流.
    3. ObjectInputStream:对象字节输入流.

示意图

image-20220204204620276.

FileInputStream介绍

示例代码

/**
 * @author 软柠柠吖
 * @date 2022/2/4
 * 演示FileInputStream 的使用(字节输入流  文件--> 程序)
 */
public class FileInputStream_ {

	/**
	 * 演示读取文件...
	 * 单个字节读取,效率低
	 * 优化-> 使用read(byte[] b)
	 */
	@Test
	public void readFile01() {
		FileInputStream fileInputStream = null;
		String filePath = "D:\\hello.txt";
		int readData = 0;
		try {
			// 创建 FileInputStream 对象,用于读取 文件
			fileInputStream = new FileInputStream(filePath);
			// 从该输入流只读取一个字节的数据。 如果没有输入可用,此方法将阻止。
			// 如果返回-1, 表示读取完毕。
			// 在UTF-8中,一个汉字由3个字节表示,用字节输入流读取汉字会出现乱码问题
			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();
			}
		}

	}

	/**
	 * 优化
	 * 使用read(byte[] b) 读取文件,提高效率
	 */
	@Test
	public void readFile02() {
		FileInputStream fileInputStream = null;
		String filePath = "D:\\hello.txt";
		// 字节数组
		byte[] buff = new byte[8];// 一次读取8个字节
		int readLen = 0;
		try {
			// 创建 FileInputStream 对象,用于读取 文件
			fileInputStream = new FileInputStream(filePath);
			// 从该输入流读取最多b.length字节的数据到字节数组。 此方法将阻塞,直到某些输入可用。
			// 如果返回-1,表示读取完毕
			// 如果读取正常,返回实际读取的字节数
			while ((readLen = fileInputStream.read(buff)) != -1) {
				System.out.print(new String(buff, 0, readLen));// 转成String 显示
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭文件流,释放资源
			try {
				if (fileInputStream != null) {
					fileInputStream.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

OutputStream:字节输出流

示意图

image-20220204220813301.

FileOutputStream介绍

示例代码

public class FileOutputStream_ {
    /**
     * 演示使用FileOutputStream 将数据写到文件中,
     * 如果该文件不存在,则创建文件
     */
    @Test
    public void writeFile() {

        String filePath = "D:\\a.txt";
        // 创建FileOutputStream 对象
        FileOutputStream fos = null;

        try {
            // 得到 FileOutputStream对象
            // 说明
            // 1.new FileOutputStream(filePath) 创建方式,当写入内容时,会覆盖原来的文件内容
            // 2.new FileOutputStream(filePath, true) 创建方式,当写入内容时,是追加到文件的末尾,而非覆盖
            // append: 追加(true为追加,false为覆盖)
            fos = new FileOutputStream(filePath,true);
            // 1.写入一个字节
            // fos.write('H');

            // 2.写入字符串
            String str = "Hello,World";
            // "韩顺平".getBytes() 把 字符串->字节数组
            // fos.write(str.getBytes());
            fos.write(str.getBytes(), 0, str.length());// 等价于 fos.write(str.getBytes());

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fos != null) {
                    fos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

文件拷贝

示意图

image-20220206115600445.

示例代码

public class FileCopy {
	/**
	 * 完成 文件拷贝,将D:\\wb.png 拷贝到 C:\\
	 * 思路分析
	 * 1.创建文件的输入流,将文件读入到 Java程序
	 * 2.创建文件的输出流,将读取到的文件数据,写入到指定文件(要指定文件名,不能只指定父类地址)
	 */
	@Test
	public void fileCopy() {
		// 图片所在地址
		String src = "D:\\wb.png";
		// 目的地址
		String dest = "d:\\wb2.png";
		// 输入流
		FileInputStream fileInputStream = null;
		// 输出流
		FileOutputStream fileOutputStream = null;

		int readLen = 0;
		// 定义一个字节数组,提高读取效率
		// 1k = 1024byte
		byte[] buff = new byte[1024];
		try {
			fileInputStream = new FileInputStream(src);
			// 这里不能加true,不是说加上true后,程序就不能运行
			// 而是加上true后,每次运行后,原来的文件都会变大,在文件的末尾追加数据
			// 总结:
			// 1.复制文件时,输出流不能使用追加,即不能加true
			// 2.当向文件添加数据时,需要追加,那就加上true
			fileOutputStream = new FileOutputStream(dest);
			while ((readLen = fileInputStream.read(buff)) != -1) {
				// 读取到后,就写入到文件,通过 fileOutputStream
				// 即 一边读一边写
				fileOutputStream.write(buff, 0, readLen);// 一定要使用这个方法

			}
			System.out.println("复制完成..");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭输入流和输出流,释放资源
			if (fileInputStream != null) {
				try {
					fileInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (fileOutputStream != null) {
				try {
					fileOutputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

文件字符流说明

FileReader 和 FileWriter 介绍

  • FileReaderFileWriter 是字符流,即按照字符来操作io.

FileReader

FileReader示意图

image-20220205101655400.

FileReader常用方法

  1. new FileReader(File/String) 通过 文件对象/字符串 创建FileReader对象
  2. read:每次读取单个字符,返回该字符的int值,如果到达文件末尾返回 -1.
  3. read(char[]):批量读取多个字符到字符数组,返回读取到的字符数,如果到达文件末尾返回-1.
  • 相关API:
    1. new String(char[]):将char[] 转换成 String.
    2. new String(char[], off, len):将char[] 的指定部分转换成String.

FileReader示例代码

public class FileReader01 {
	/**
	 * 使用单个字符读取文件
	 */
	@Test
	public void readFile() {
		String filePath = "D:\\story.txt";
		// 1.创建FileReader 对象
		FileReader reader = null;
		int c = 0;
		try {
			reader = new FileReader(filePath);
			// 循环读取 使用read, 单个字符读取,效率低
			// read方法 返回的是文件中的字符或者到达文件末尾的-1, 所以用char 无法接收
			// 但是字符的本质就是一个数字,所以用 int来接收这两种情况
			while ((c = reader.read()) != -1) {
				System.out.print((char) c);
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	/**
	 * 使用 字符数组 读取文件
	 */
	@Test
	public void readFile2() {
		String filePath = "D:\\story.txt";
		// 1.创建FileReader 对象
		FileReader reader = null;
		// 字符数组
		char[] buff = new char[1024];
		int readLen = 0;
		try {
			reader = new FileReader(filePath);
			// 循环读取, 使用read(buff), 返回的是实际读取到的字符数, 效率高
			// 如果返回-1,说明到文件末尾
            // 软柠柠吖 --> 4个字符(在UTF-8中 占4*3=12个字节)
            // 一个字母(A...)是一个字符(char),一个汉字(软...)也是一个字符(char),
            // 但是字符(char)具体所占的字节数(byte),需要根据当前编码计算
			while ((readLen = reader.read(buff)) != -1) {
				System.out.print(new String(buff, 0, readLen));
			}

		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

FileWriter

FileWriter示意图

image-20220205101615210.

FileWriter常用方法

  1. new FileWriter(File/String)覆盖模式,相当于流的指针在首端.
  2. new FileWriter(File/String, true)追加模式,相当于流的指针在尾端.
  3. write(int):写入单个字符.
  4. write(char[]):写入指定数组.
  5. write(char[], off, len):写入指定数组的指定部分.
  6. write(String):写入整个字符串.
  7. write(String, off, len):写入字符串的指定部分.
  • 相关API:

    1. String类:toCharArray:将String 转换成 char[].
  • 注意

    FileWriter使用后,必须要关闭(close) 或 刷新(flush),否则写入不到指定的文件

FileWriter示例代码

public class FileWriter01 {
	@Test
	public void writeFile() {
		String filePath = "D:\\note.txt";
		FileWriter fileWriter = null;
		char[] chars = {'a', 'b', 'c'};
		try {
            // 
			// 1.fileWriter = new FileWriter(filePath);
			//   此时 fileWriter 的append为默认值false, 新数据覆盖原文件的内容(旧数据被新数据覆盖)
			//   但是同一个fileWriter对象 调用write方法,则是内容追加写入
			// 2.fileWriter = new FileWriter(filePath, true);
			//   此时 fileWriter 的append为true, 追加,在原文件结尾追加新数据(保留旧数据)
			//   同样,同一个fileWriter对象 调用 write方法,则是内容追加写入
			fileWriter = new FileWriter(filePath);// 默认是覆盖写入

			// 同一个 FileWriter对象,调用不同的write方法,依旧是内容追加写入
			// write(int): 写入单个字符
			fileWriter.write('H');
			// write(char[]): 写入指定数组
			fileWriter.write(chars);
			// write(char[],off,len):写入指定数组的指定部分
			fileWriter.write("软柠柠吖".toCharArray(), 0, 3);
			// write(string):写入整个字符串
			fileWriter.write(" 你好,北京~");
			// write(string,off,len):写入字符串的指定部分
			fileWriter.write("上海天津", 0, 2);

			// 总结
			// 1.在数据量大的情况下,可以使用循环操作.
			// 2.如果需要新数据 追加写入 到文件中,则创建对象时 显示指定append 为true(保留旧数据)
			// 3.同一个输出流对象 调用write方法,不论append 为true还是false,都是内容追加写入文件(先到先得)
			// 4.如果不关闭流,那么无法真正的往文件中写入数据
			
			fileWriter.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 对于FileWriter,一定要关闭流,或者flush 才能真正的把数据写入到文件
			if (fileWriter != null) {
				try {
					// 关闭文件流,等价 flush() + 关闭
					fileWriter.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

节点流和处理流

基本介绍

  1. 节点流可以从一个特定的数据源 读写数据,如FileReader(只能对文件操作)、FileWriter(也只能对文件操作).
  2. 处理流(也叫包装流)是 "连接" 在已存在的流(节点流或处理流) 之上,为程序提供更为强大的读写功能,也更加灵活。如BufferedReaderBufferedWriter.

字节流VS字符流 VS 节点流VS处理流 关系图

image-20220211115335736.

节点流示意图

image-20220209164455837.

InputStream示意图

image-20220209170337642.

OutputStream示意图

image-20220209170056883.

Reader示意图

image-20220209165504139.

Writer示意图

image-20220209165602529.

处理流(包装流)示意图

image-20220209165339442.

节点流和处理流的区别和联系

  1. 节点流是底层流/低级流,直接跟 指定的 数据源相连
  2. 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出。
  3. 处理流(也叫包装流) 对字节流进行包装,使用了修饰器设计模式不会直接与数据源相连

处理流使用的设计模式

AbstractReader类 相当于 Reader类
public abstract class AbstractReader {// 抽象类 类似Reader
    // 使用read方法,同一管理
	public abstract void read();
}
CustomFileReader类 相当于 FileReader类
// 节点流, 因为CustomFileReader 与具体的数据源相关, 只能读取文件,功能较弱
public class CustomFileReader extends AbstractReader {// 节点流

	@Override
	public void read() {
		System.out.println("读取文件~");
	}
}
CustomStringReader类 相当于StringReader类
// 节点流, 因为CustomStringReader 与具体的数据源相关, 只能读取字符串,功能较弱
public class CustomStringReader extends AbstractReader {

	@Override
	public void read() {
		System.out.println("读取字符串~~");
	}
}
CustomBufferedReader类 相当于 BufferedReader类 ==> 修饰器模式
// 处理流/包装流
public class CustomBufferedReader extends AbstractReader {

	// 属性是AbstractReader 类型
	private AbstractReader in;

	// 接收AbstractReader 子类对象
	public CustomBufferedReader(AbstractReader in) {
		this.in = in;
	}

	@Override
	public void read() {
		// 动态绑定,绑定运行类型(由构造器传入的)
		in.read();
	}
    
	// 让方法更加灵活,多次读取文件,或者加缓冲char[]...
	public void reads(int num) {
		for (int i = 0; i < num; i++) {
			// 动态绑定,绑定运行类型(由构造器传入的)
			in.read();
		}
	}
}
Test01类 测试方法
public class Test01 {
	public static void main(String[] args) {
		// 读取文件
		CustomBufferedReader customBufferedReader1 = new CustomBufferedReader(new CustomFileReader());
		customBufferedReader1.reads(3);

		// 读取字符串
		CustomBufferedReader customBufferedReader2 = new CustomBufferedReader(new CustomStringReader());
		customBufferedReader2.reads(5);
	}
}

处理流的功能体现(2方面)

  1. 性能的提高:主要以增加缓冲的方式来提高输入输出效率。
  2. 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活。

处理流-BufferedReader 和 BufferedWriter -->字符(char)

基本介绍

  • BufferedReaderBufferedWriter 属于字符流,是按照 字符 来读取数据的。
  • 尽量处理文本文件(高效),不要用来处理二进制文件(出现问题).
  • 关闭流时,只需要关闭外层流(处理流)即可 (底层会帮你关闭节点流).
  • 真正工作的还是节点流,处理流只是做了包装。

使用BufferedReader 读取 文本文件,并显示在控制台

public class BufferedReader01 {

	@Test
	public void readFile() {
		// 文件地址
		String filePath = "D:\\1.txt";
		BufferedReader bufferedReader = null;
		try {
			// 创建BufferedReader对象(java.io)
			bufferedReader = new BufferedReader(new FileReader(filePath));
			// 按行读取,效率高
			String line = null;
			// 说明
			// 1.bufferedReader.readLine() 是按行读取文件
			// 2.当返回null 时,表示文件读取完毕
			while ((line = bufferedReader.readLine()) != null) {
				System.out.println(line);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭流,这里注意,只需要关闭 BufferedReader,因为底层会自动的去关闭 节点流
			/*  bufferedReader.close();源码
				public void close () throws IOException {
					synchronized (lock) {
						if (in == null)
							return;
						try {
							in.close();// in 就是我们传入的 new FileReader(filePath)对象,关闭了节点流
						} finally {
							in = null;
							cb = null;
						}
					}
				}
			*/
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

使用 BufferedWriter 将 文本 写入到文件中

  • 思考字节流 可以操作二进制文件,那可以操作文本文件吗???当然可以,完全.
public class BufferWriter01 {
	
	@Test
	public void writeFile() {
		String filePath = "D:\\ok.txt";
		BufferedWriter bufferedWriter = null;
		try {
			// 创建BufferedWriter对象(java.io)
			// 说明:
			// 1.new FileWriter(filePath, true): 表示以追加的方式写入
			// 2.new FileWriter(filePath): 表示以覆盖的方式写入
			bufferedWriter = new BufferedWriter(new FileWriter(filePath));// 覆盖的方式
			// bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));// 追加的方式
			bufferedWriter.write("软柠柠吖1~~");
			// 插入一个和系统相关的换行符
			bufferedWriter.newLine();
			bufferedWriter.write("软柠柠吖2~~");
			bufferedWriter.newLine();
			bufferedWriter.write("软柠柠吖3~~");
			bufferedWriter.newLine();

			bufferedWriter.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 说明:关闭外层流即可,传入的 new FileWriter(filePath), 会在底层关闭
			/*  bufferedWriter.close();源码
				public void close() throws IOException {
			        synchronized (lock) {
			            if (out == null) {
			                return;
			            }
			            // 注意区分 try 与 try()
			            // try (Writer w = out) try块退出时,会自动调用w.close(); 即out.close();
			            // 自动关闭资源
			            try (Writer w = out) {
			                flushBuffer();
			            } finally {
			                out = null;
			                cb = null;
			            }
			        }
			    }
			 */
			if (bufferedWriter != null) {
				try {
					bufferedWriter.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

利用 处理流(BufferedReader 和 BufferedWriter) 实现 文本文件 的拷贝

public class BufferedCopyFile {
	@Test
	public void bufferedCopyFile() {
		// 说明
		// 1.BufferedReader 和 BufferedWriter 是按照 字符 操作
		// 2.不要去操作 二进制文件[声音、视频、doc、word、pdf],可能造成文件损坏
		String srcFilePath = "D:\\ok.txt";
		String destFilePath = "D:\\ok2.txt";
		BufferedReader bufferedReader = null;
		BufferedWriter bufferedWriter = null;
		String line = null;
		try {
			bufferedReader = new BufferedReader(new FileReader(srcFilePath));
			bufferedWriter = new BufferedWriter(new FileWriter(destFilePath));
			// 边读边写
			// 说明:readLine 读取一行的内容,但是没有换行,
			//      因此我们通常 配合使用newLine 添加系统换行.
			while ((line = bufferedReader.readLine()) != null) {
				// 每读取一行,就写入
				bufferedWriter.write(line);
				// 插入一个系统换行符
				bufferedWriter.newLine();
			}
			System.out.println("拷贝Ok~");
			bufferedWriter.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭流
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			try {
				if (bufferedWriter != null) {
					bufferedWriter.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

处理流-BufferedInputStream 和 BufferedOutputStream-->字节(byte)

BufferedInputStream 基本介绍

  • BufferedInputStream字节流,在创建BufferedInputStream对象时,会创建一个内部缓冲区数组.

BufferedOutputStream 基本介绍

  • BufferedOutputStream字节流,实现缓冲的输出流,可以将多个字节写入底层输出流中,而不必对每次字节写入调用底层系统

编程完成图片/音乐[二进制文件] 的拷贝

public class BufferedFileCopy {
	
	@Test
	public void bufferedFileCopy() {
		String srcFilePath = "D:\\1.webp";
		String destFilePath = "D:\\2.webp";

		BufferedInputStream bis = null;
		BufferedOutputStream bos = null;
		try {
			// 创建BufferedInputStream对象 和 BufferedOutputStream对象
			// 因为 FileInputStream 是 InputStream 子类
			bis = new BufferedInputStream(new FileInputStream(srcFilePath));
			bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
			// 创建字节缓冲数组
			byte[] buff = new byte[1024];
			int readLen = 0;
			// 循环的读取文件,并写入到 destFilePath
			// 当返回 -1 时,就表示文件读取完毕
			while ((readLen = bis.read(buff)) != -1) {
				bos.write(buff, 0, readLen);
			}
			System.out.println("拷贝Ok~");
			bos.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			// 关闭流,关闭外层的处理流即可,底层会去关闭节点流
			if (bis != null) {
				try {
					bis.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			if (bos != null) {
				try {
					bos.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

对象处理流-ObjectInputStream 和 ObjectOutputStream

需求

  1. int num = 100 这个 int 数据保存到文件中,注意不是 100 数字,而是 int 100,并且,能够从文件中直接恢复 int 100.
  2. Dog dog = new Dog("小黄", 3); 这个 dog对象 保存到 文件 中,并且能够从文件中恢复.
  3. 上面的要求,就是 能够将 基本数据类型 或者 对象 进行 序列化 和 反序列化操作

序列化和反序列化

  1. 序列化就是在保存数据的时候,保存数据的值数据类型
  2. 反序列化就是恢复数据时,恢复数据的值数据类型
  3. 需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一
    • Serializable // 这是一个标记接口,没有方法【推荐使用
    • Externalizable // 该接口有方法需要实现,因此我们一般实现上面的 Serializable 接口.
  4. 一个对象希望通过对象流书写,则需要序列化

示意图

image-20220211082108288.

基本介绍

  1. 功能:提供了对 基本数据类型 或 对象类型 的 序列化和反序列化 的方法.
  2. ObjectInputStream: 提供 反序列化 功能
  3. ObjectOutputStream: 提供 序列化 功能
ObjectInputStream示意图
  • 该图体现了 修饰器模式ObjectInputStream处理流.

image-20220211082523285.

ObjectOutputStream示意图
  • 该图体现了 修饰器模式ObjectOutputStream处理流.

image-20220211082836491.

使用 ObjectOutputStream 序列化 基本数据类型和 一个Dog对象(name, age), 并保存到data.dat文件中

ObjectOutputStream01类
// 演示ObjectOutputStream 的使用,完成数据序列化
public class ObjectOutputStream01 {
	
	@Test
	public void m1() {
		// 序列化后,保存的文件格式,不是纯文本,而是按照它的格式来保存的
		String filePath = "D:\\data.dat";
		ObjectOutputStream objectOutputStream = null;
		try {
			objectOutputStream = new ObjectOutputStream(new FileOutputStream(filePath));
			// 序列化数据到 D:\data.dat
			objectOutputStream.write(100);
			// int -> Integer (实现了 Serializable)
			objectOutputStream.writeInt(100);
			// boolean -> Boolean (实现了 Serializable)
			objectOutputStream.writeBoolean(true);
			// char -> Character (实现了 Serializable)
			objectOutputStream.writeChar('a');
			// double -> Double (实现了 Serializable)
			objectOutputStream.writeDouble(9.6);
			// String (实现了 Serializable)
			objectOutputStream.writeUTF("软柠柠吖");
			// 保存一个dog对象
			objectOutputStream.writeObject(new Dog("旺财", 5));
			// objectOutputStream.writeObject(dog2);

			System.out.println("数据保存完毕(序列化机制)~");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (objectOutputStream != null) {
				try {
					objectOutputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
Dog类
// 如果需要序列化某个类的对象,实现 Serializable 接口
public class Dog implements Serializable {
	String name;
	int age;

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

	@Override
	public String toString() {
		return "Dog{" +
				"name='" + name + '\'' +
				", age=" + age +
				'}';
	}
}

使用ObjectInputSteam 读取 data.dat 并 反序列化恢复数据

public class ObjectInputStream01 {
	@Test
	public void m2() {
		// 指定反序列化文件
		String filePath = "D:\\data.dat";
		ObjectInputStream objectInputStream = null;

		try {
			objectInputStream = new ObjectInputStream(new FileInputStream(filePath));
			// 读取
			// 1.读取(反序列化)的顺序 需要和你 保存数据(序列化)的顺序一致
			// 否则会出现异常
			System.out.println(objectInputStream.readInt());
			System.out.println(objectInputStream.readBoolean());
			System.out.println(objectInputStream.readChar());
			System.out.println(objectInputStream.readDouble());
			System.out.println(objectInputStream.readUTF());
			Object dog = objectInputStream.readObject();
			System.out.println("运行类型=" + dog.getClass());
			System.out.println("dog信息=" + dog);// 底层 Object -> Dog

			// 这里是特别重要的细节
			// 1.如果我们希望调用Dog的方法,需要向下转型
			// 2.需要我们将 Dog类的定义,放到能够引用的位置
			Dog dog2 = (Dog) dog;
			System.out.println(dog2.getName());

		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		} finally {
			// 关闭流,关闭外层流即可,底层会自动关闭 FileInputStream 流
			if (objectInputStream != null) {
				try {
					objectInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

注意事项和细节说明

  1. 读写顺序要一致
  2. 一个对象希望通过对象流书写,则需要序列化
  3. 要求实现序列化或反序列化对象,需要 实现 Serializable 接口。
  4. 序列化的类中 建议添加 SerialVersionUID,为了提高版本的兼容性。private static final long serialVersionUID = 1L; // serialVersionUID 序列化的版本号,可以提高兼容性
  5. 序列化对象时,默认将里面 所有属性 都进行序列化但是除了 statictransient 修饰的成员。
  6. 序列化对象时,要求里面属性的类型 也需要实现序列化接口
  7. 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化。

标准输入输出流

Standard 标准.

基本介绍

编译类型 运行类型 默认设备
System.in 标准输入 InputStream BufferedInputStream 键盘
System.out 标准输出 PrintStream PrintStream 显示器

示例代码

public class InputAndOutput {
	public static void main(String[] args) {
		// 说明
		// 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.out 编译类型 PrintStream
		// System.out 运行类型 PrintStream
		System.out.println(System.out.getClass());

		// 传统方法 System.out.println("");// 是使用 out对象 将数据输出到 显示器
		System.out.println("hello, 软柠柠吖~");

		// 传统方法 Scanner 是从 标准输入 键盘接收数据
		Scanner myScanner = new Scanner(System.in);
		System.out.println("输入内容");
		String str = myScanner.next();
		System.out.println("str=" + str);
	}
}

转换流-InputStreamReader 和 OutputStreamWriter

transformation 转型.

基本介绍

  1. InputStreamReaderReader的子类,可以将InputStream(字节流) 包装成(转换) Reader(字符流).
  2. OutputStreamWriterWriter的子类,可以将OutputStream(字节流) 包装成 Writer(字符流).
  3. 当处理纯文本数据时,使用 字符流 效率更高,并且可以有效解决中文乱码问题,所有 建议 将字节流转换成字符流。
  4. 可以在使用时指定编码格式(比如 utf-8gbkgb2312ISO8859-1 等)

读文件-示例代码

  • 编程将 字节流FileInputStream 包装成(转换成) 字符流 InputStreamReader,对文件进行读取(按照 utf-8/gbk格式),进而再包装成 BufferedReader.
错误示范
// 看一个中文乱码问题
public class CodeQuestion {
	// 使用字节流读取中文文件 出现乱码问题
	@Test
	public void readFile() {
		String filePath = "D:\\ok.txt";
		FileInputStream fileInputStream = null;
		try {
			fileInputStream = new FileInputStream(filePath);
			InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
			byte[] buff = new byte[8];
			int readLen = 0;
			while ((readLen = fileInputStream.read(buff)) != -1) {
				System.out.println(new String(buff, 0, readLen));
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (fileInputStream != null) {
				try {
					fileInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

    // 由于格式不正确导致读取文件出现乱码问题
	@Test
	public void readFile2() {
		String filePath = "D:\\ok.txt";

		BufferedReader bufferedReader = null;
		try {
			bufferedReader = new BufferedReader(new FileReader(filePath));
			String line = null;
			while ((line = bufferedReader.readLine()) != null) {
				System.out.println(line);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
正确的代码案例
    // 正确的代码
    @Test
    public void readFile3() {
        String filePath = "D:\\ok.txt";
        InputStreamReader inputStreamReader = null;
        BufferedReader bufferedReader = null;
        try {
            // 1.把 FileInputStream 转成 InputStreamReader
            // 2.指定编码 gbk
            //inputStreamReader = new InputStreamReader(new FileInputStream(filePath), "gbk");
            // 3.把 InputStreamReader 传入 BufferedReader
            //bufferedReader = new BufferedReader(inputStreamReader);

            // 将2 和 3 合在一起写
            bufferedReader = new BufferedReader(new InputStreamReader(
                new FileInputStream(filePath), "gbk"));
            // 4.读取
            String line = null;
            System.out.println("==读取内容==");
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭外层流
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

写文件-示例代码

// 1.把 FileOutputStream 字节流,转成字符流 OutputStreamWriter
// 2.指定处理的编码 gbk/utf-8(utf8)
public class OutputStreamWriter01 {
	
	@Test
	public void writeFile() {
		String destFilePath = "D:\\lemon.txt";
		// utf-8(utf8)、gbk
		String charSet = "gbk";
		OutputStreamWriter outputStreamWriter = null;
		try {
			outputStreamWriter = new OutputStreamWriter(new FileOutputStream(destFilePath), charSet);
			outputStreamWriter.write("你好,软柠柠吖");

			System.out.println("按照" + charSet + "保存文件成功~");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (outputStreamWriter != null) {
				try {
					outputStreamWriter.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

打印流-PrintStream 和 PrintWriter

  • 打印流只有 输出流,没有输入流.

PrintStream示意图(字节流)

image-20220211163305974.

字节打印流 示例代码

public class PrintStream1 {
	@Test
	public void printStream() throws IOException {
		// PrintStream 字节打印流/输出流
		PrintStream out = System.out;
		// 在默认情况下,PrintStream 输出数据的位置是 标准输出,即显示器
		/*
			public void print(String s) {
		        if (s == null) {
		            s = "null";
		        }
		        write(s);
		    }
		 */
		out.print("软柠柠吖");
		// 因为 print底层使用的是 write,所以我们可以 直接调用write 进行打印/输出
		out.write(" 你好,软柠柠吖".getBytes());

		// 我们可以去修改 打印流 输出的位置/设备
		// 1.输出修改成到 D:\t2.txt
		// 2."你好,软柠柠吖" 就会输出到 D:\t2.txt
		// 3.源码
		/*
			public static void setOut(PrintStream out) {
		        checkIO();
		        setOut0(out);// native 方法,修改了out
		    }
		 */
		System.setOut(new PrintStream("D:\\t2.txt"));
		System.out.println("你好,软柠柠吖");
		
		out.close();
	}
}

PrintWriter示意图(字符流)

image-20220211163446111.

字符打印流 示例代码

public class PrintWriter01 {
	public static void main(String[] args) throws IOException {
		//PrintWriter printWriter = new PrintWriter(System.out);// 标准输出,显示器显示
		PrintWriter printWriter = new PrintWriter(new FileWriter("D:\\t1.txt"));
		printWriter.println("hi, 北京你好~~~~~");

		// flush + 关闭流,才会将数据写入到文件..
		printWriter.close();
	}
}

Properties 配置文件

传统方案代码 - 繁琐,不方便

public class Properties01 {
	// 传统方案
	public static void main(String[] args) throws IOException {
		// 读取mysql.properties 文件,并得到 ip,user和pwd
		BufferedReader bufferedReader = new BufferedReader(new FileReader("src\\mysql.properties"));
		String line = null;
		// 循环读取
		while ((line = bufferedReader.readLine()) != null) {
			String[] split = line.split("=");
			if ("ip ".equals(split[0])) {
				System.out.println(split[0] + "值为:" + split[1]);
			}
		}
		bufferedReader.close();
	}
}

Properties类

基本介绍

  1. 专门用于读写配置文件的集合类

    配置文件格式

    键=值

    键=值

  2. 注意:键值对不需要有空格,值不需要用引号引起来。默认类型是String.

示意图

image-20220212101639183.

Properties的常用方法

  1. load:加载配置文件的键值对到Properties对象。
  2. list:将数据显示到指定设备/流对象。
  3. getProperty(key):根据键获取值。
  4. setProperty(key, value):设置键值对到Properties对象。
  5. store:将Properties 中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为Unicode码。

示例代码

读取properties文件
public class Properties02 {
	public static void main(String[] args) throws IOException {
		// 使用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);
	}
}
创建、修改Properties配置文件
public class Properties03 {
	public static void main(String[] args) throws IOException {
		// 使用Properties 类来 创建配置文件,修改配置文件内容

		// 创建Properties 对象
		Properties properties = new Properties();
		// 创建
		// 1.如果该文件没有key 就是创建
		// 2.如果该文件有key, 就是修改
		properties.setProperty("charSet", "utf8");
		// 注意 保存时,是中文的Unicode码
		properties.setProperty("user", "汤姆");
		properties.setProperty("pwd", "55555");
		// 将k-v 存储到文件中即可 内存->文件
		properties.store(new FileOutputStream("src\\mysql2.properties"), null);
		System.out.println("保存配置文件成功~");
	}
}

课后练习

创建文件夹+创建文件+向文件中写入信息

public class HomeWork01 {

	@Test
	public void makeFile() {
		// 创建目录
		String directoryPath = "D:\\mytemp";
		File dir = new File(directoryPath);
		if (!(dir.exists() && dir.isDirectory())) {
			if (dir.mkdirs()) {
				System.out.println(directoryPath + " 目录创建OK~~");
			} else {
				System.out.println("目录创建失败!");
			}
		} else {
			System.out.println(directoryPath + " 目录已经存在");
		}

		// 创建文件 D:\mytemp\hello.txt
		String filePath = directoryPath + "\\hello.txt";
		File file = new File(filePath);
		if (!(file.exists() && file.isFile())) {
			try {
				if (file.createNewFile()) {
					System.out.println(file.getName() + " 文件创建成功~~");
				} else {
					System.out.println("文件创建失败!");
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		} else {
			System.out.println(file.getName() + " 文件已经存在");
		}

		// 向文件中写入hello,world
		// 使用BufferedWriter 字符输出流 写入信息
		BufferedWriter bufferedWriter = null;
		try {
			bufferedWriter = new BufferedWriter(new FileWriter(file));
			bufferedWriter.write("hello,world~");
			bufferedWriter.flush();

			System.out.println("写入OK~");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (bufferedWriter != null) {
				try {
					bufferedWriter.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

读取文件 - (给文件加行号 + 按指定编码读取文件)

public class HomeWork02 {

	@Test
	public void readFile() {
		// 文件路径
		String filePath = "D:\\data.txt";
		BufferedReader bufferedReader = null;
		try {
			// 创建 BufferedReader对象
			// 默认编码方式 utf-8 读取文件
			// bufferedReader = new BufferedReader(new FileReader(filePath));

			// 按照指定编码方式 读取文件 
			String charSet = "gbk";
			// (转换流) FileInputStream->InputStreamReader[可以指定编码]->BufferedReader
			bufferedReader = new BufferedReader(new InputStreamReader(
					new FileInputStream(filePath), charSet));
			// 行号
			int lineNum = 1;
			// 按行读取
			String line = null;
			while ((line = bufferedReader.readLine()) != null) {
				System.out.println(lineNum++ + " " + line);
			}
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

Properties配置文件(创建和读取)+对象流(序列化和反序列化)

public class HomeWork03 {

	@Test
	public void useProperties() {
		// 创建 Properties 对象
		Properties properties = new Properties();
		// 设置信息
		properties.setProperty("name", "tom");
		properties.setProperty("age", "5");
		properties.setProperty("color", "red");
		// 创建配置文件
		String dogPropertiesPath = "src\\dog.properties";
		try {
			properties.store(new FileOutputStream(dogPropertiesPath), null);
			System.out.println("配置文件创建OK~");
		} catch (IOException e) {
			e.printStackTrace();
		}
//********************************************************************************
		// 加载配置文件
		properties = new Properties();
		try {
			properties.load(new FileReader(dogPropertiesPath));
		} catch (IOException e) {
			e.printStackTrace();
		}
		// 创建Dog对象
		Dog dog = new Dog(properties.getProperty("name")
				, Integer.parseInt(properties.getProperty("age"))
				, properties.getProperty("color"));
		System.out.println(dog);
//*******************************************************************************
		// 将dog对象 序列化输出到文件
		String filePath = "D:\\dog.dat";
		ObjectOutputStream objectOutputStream = null;
		try {
			objectOutputStream = new ObjectOutputStream(new FileOutputStream(filePath));
			objectOutputStream.writeObject(dog);

			objectOutputStream.flush();
			System.out.println("序列化写入OK~");
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (objectOutputStream != null) {
				try {
					objectOutputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
//************************************************************************
		// 将文件中的dog对象 反序列化输入到控制台
		ObjectInputStream objectInputStream = null;
		try {
			objectInputStream = new ObjectInputStream(new FileInputStream(filePath));
			Object obj = objectInputStream.readObject();
			System.out.println(obj.getClass());
			System.out.println(obj);
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		} finally {
			if (objectInputStream != null) {
				try {
					objectInputStream.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}

class Dog implements Serializable {
	private String name;
	private int age;
	private String color;

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

	@Override
	public String toString() {
		return "Dog{" +
				"name='" + name + '\'' +
				", age=" + age +
				", color='" + color + '\'' +
				'}';
	}
}

坦克大战3.0

代码在gitee

网络编程

网络通信

  1. 概念:两台设备之间通过 网络 实现 数据传输.
  2. 网络通信:将数据通过网络从一台设备传输到另一台设备.
  3. java.net 包下提供了一系列的类或接口,供程序员使用,完成网络通信.

网络

  1. 概念:两台或多台设备通过一定物理设备连接起来构成了网络.
  2. 根据 网络的覆盖范围 不同,对网络进行分类:
    • 局域网:覆盖范围最小,仅仅覆盖一个教室或一个机房。
    • 城域网:覆盖范围较大,可以覆盖一个城市。
    • 广域网:覆盖范围很大,可以覆盖全国,甚至全球,万维网是广域网的代表。

ip地址

  1. 概念:用于唯一标识网络中的每台计算机/主机。

  2. 查看ip地址:ipconfig

  3. ip地址的表示形式:点分十进制 xx.xx.xx.xx

    • IPv4:4个字节(32位)表示,1个字节的范围是:0~255
    • IPv6:16个字节(128位),是IPv4地址长度的4倍,
  4. 每一个十进制数的范围:0~255

  5. ip地址的组成=网络地址+主机地址,比如:192.168.16.69

  6. IPv6 是互联网工程任务组设计的用于替代IPv4 的下一代IP协议,其地址数量号称可以为全世界的每一粒沙子编上一个地址。

  7. 由于IPv4 最大的问题在于网络地址资源有限,严重制约了互联网的应用和发展。IPv6 的使用,不仅能解决网络地址资源数量的问题,而且也解决了多种接入设备连入互联网的障碍。

  8. 类型(IPv4) 范围
    A 0.0.0.0 - 127.255.255.255
    B 128.0.0.0 - 191.255.255.255
    C 192.0.0.0 - 223.255.255.255
    D 224.0.0.0 - 239.255.255.255
    E 240.0.0.0 - 247.255.255.255
  9. 特殊地址:127.0.0.1 表示本机地址

域名

  1. www.baidu.com
  2. 好处:为了方便记忆,解决记ip的困难。
  3. 概念:将ip地址映射成域名。

端口号

  1. 概念:用于标识计算机上某个特定的网络程序
  2. 表示形式:以整数形式,范围 0~65535 【2个字节表示端口 0~2^16-1】
  3. 0~1024 已经被占用,比如 ssh 22,ftp 21,smtp 25,http 80
  4. 常见的网络程序端口号:
    • tomcat:8080
    • mysql:3306
    • oracle:1521
    • sqlserver:1433
  5. 端口域名示意图:

image-20220301202156388.

网络通信协议

  • 协议(tcp/ip)

    TCP/IP (Transmission Control Protocol / Internet Protocol) 的简写,中文译文为 传输控制协议/因特网互联协议,又叫网络通讯协议,这个协议是Internet最基本的协议、Internet国际互联网络的基础,简单地说,就是由网络层的IP协议和传输层的TCP协议组成的。

  • 示意图

image-20220301220539067.

TCP 和 UDP

  • TCP协议:传输控制协议

    1. 使用TCP协议前,须先建立TCP连接,形成传输数据通道。
    2. 传输前,采用 "三次握手" 方式,是可靠的
    3. TCP协议进行通信的两个应用进程:客户端、服务端。
    4. 在连接中可进行大数据量的传输
    5. 传输完毕,需释放已建立的连接效率低
  • UDP协议:用户数据协议

    1. 将数据、源、目的封装成数据包,不需要建立连接。
    2. 每个数据包的大小限制在64K内,不适合传输大量数据。
    3. 因无需连接,故是不可靠的
    4. 发送数据结束时无需释放资源(因为不是面向连接的),速度快
    5. 举例:厕所通知、发短信。

InetAddress类

相关方法

  1. 获取本机InetAddress对象:getLocalHost
  2. 根据指定主机名/域名获取ip地址对象:getByName
  3. 获取InetAddress对象的主机名:getHostName
  4. 获取InetAddress对象的地址:getHostAddress

示例代码

// 演示InetAddress 类的使用
public class API {
    public static void main(String[] args) throws UnknownHostException {
        // 1.获取本机InetAddress对象
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);// SunnyYang/192.168.240.1

        // 2.根据指定的主机名,获取 InetAddress对象
        InetAddress host1 = InetAddress.getByName("SunnyYang");
        System.out.println(host1);// SunnyYang/192.168.240.1

        // 3.根据域名返回 InetAddress对象
        // 比如 www.baidu.com 对应的InetAddress对象
        InetAddress host2 = InetAddress.getByName("www.baidu.com");
        System.out.println(host2);// www.baidu.com/180.101.49.12

        // 4.通过 InetAddress 对象,获取对应地址
        String address = host2.getHostAddress();// IP地址
        System.out.println(address);// 180.101.49.11

        // 5.通过 InetAddress 对象,获取对应的主机名/域名
        String hostName = host2.getHostName();
        System.out.println(hostName);// www.baidu.com
    }
}

Socket

基本介绍

  1. 套接字(Socket) 开发网络应用程序被广泛采用,以至于成为事实上的标准。
  2. 通信的两端都要有Socket,是两台机器间通信的端点。
  3. 网络通信其实就是Socket间的通信。
  4. Socket允许程序 把网络连接当成一个流,数据在两个Socket间通过IO传输
  5. 一般主动发起通信的应用程序属客户端等待通信请求的为服务端

示意图

image-20220302173920982.

TCP网络通信编程

基本介绍

  1. 基于客户端—服务端的网络通信。
  2. 底层使用的是:TCP/IP协议。
  3. 应用场景举例:客户端发送数据,服务端接收并显示。
  4. 基于Socket的TCP编程

image-20220302173844946.

示例代码01-字节流

题目介绍
  1. 编写一个服务器端,和一个客户端
  2. 服务器端在 9999端口监听
  3. 客户端连接到服务器端,发送 "hello, server",然后退出。
  4. 服务器端接收到 客户端发送的 信息,输出,并退出。
示意图

image-20220302192901884.

服务端
// 服务端
public class SocketTCP01Server {
    public static void main(String[] args) throws IOException {
        // 思路
        // 1.在本机 的9999端口监听,等待连接
        //   细节:要求在本机没有其他服务在监听9999
        //   细节:这个 ServerSocket 可以通过 accept() 返回多个Socket[多个客户端连接服务器的并发]
        ServerSocket server = new ServerSocket(9999);
        System.out.println("服务端在9999端口监听,等待连接...");
        // 2.当没有客户端连接9999端口时,程序会阻塞,等待连接
        //   如果有客户端连接,则会返回Socket对象,程序继续执行
        Socket socket = server.accept();
        System.out.println("服务端 socket=" + socket.getClass());

        // 3.通过socket.getInputStream() 读取 客户端写入到数据通道的数据,并显示
        InputStream inputStream = socket.getInputStream();
        // 4.IO读取
        // 设置缓冲
        byte[] buff = new byte[1024];
        // 读取长度
        int readLen = 0;
        while ((readLen = inputStream.read(buff)) != -1) {
            // 根据读取到的实际长度,显示内容
            System.out.println(new String(buff, 0, readLen));
        }

        // 5.关闭流和socket
        inputStream.close();
        socket.close();
        server.close();
        System.out.println("服务器 退出....");
    }
}
客户端
// 客户端
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());
        outputStream.flush();

        // 4.关闭流对象和socket,必须关闭
        outputStream.close();
        socket.close();
        System.out.println("客户端 退出...");
    }
}

示例代码02-字节流

题目介绍
  1. 编写一个服务器端,和一个客户端
  2. 服务器端在 9999端口监听
  3. 客户端连接到服务器端,发送 "hello, server",并接收服务器端回发的 "hello client",再退出。
  4. 服务器端接收到 客户端发送的 信息,输出,并发送 "hello, client", 再退出。
示意图

image-20220302201953744.

服务端
// 服务端
public class SocketTCP02Server {
    public static void main(String[] args) throws IOException {
        // 思路
        // 1.在本机 的9999端口监听,等待连接
        //   细节:要求在本机没有其他服务在监听9999
        //   细节:这个 ServerSocket 可以通过 accept() 返回多个Socket[多个客户端连接服务器的并发]
        ServerSocket server = new ServerSocket(9999);
        System.out.println("服务端在9999端口监听,等待连接...");
        // 2.当没有客户端连接9999端口时,程序会阻塞,等待连接
        //   如果有客户端连接,则会返回Socket对象,程序继续执行
        Socket socket = server.accept();
        System.out.println("服务端 socket=" + socket.getClass());

        // 3.通过socket.getInputStream() 读取 客户端写入到数据通道的数据,并显示
        InputStream inputStream = socket.getInputStream();
        // 4.IO读取
        // 设置缓冲
        byte[] buff = new byte[1024];
        // 读取长度
        int readLen = 0;
        while ((readLen = inputStream.read(buff)) != -1) {
            // 根据读取到的实际长度,显示内容
            System.out.println(new String(buff, 0, readLen));
        }
        // 设置读取结束标记
        socket.shutdownInput();

        // 5.获取socket相关联的输出流
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello client".getBytes());
        outputStream.flush();
        // 设置写入结束标记
        socket.shutdownOutput();


        // 6.关闭流和socket
        outputStream.close();
        inputStream.close();
        socket.close();
        server.close();
        System.out.println("服务器 退出....");
    }
}
客户端
// 客户端
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());
        outputStream.flush();
        // 设置写入结束标记
        socket.shutdownOutput();

        // 4.获取和socket相关联的输入流,读取数据(字节),并显示
        InputStream inputStream = socket.getInputStream();
        // 缓冲
        byte[] buff = new byte[1024];
        // 读取数据
        int readLen = 0;
        while ((readLen = inputStream.read(buff)) != -1) {
            System.out.println(new String(buff, 0, readLen));
        }
        // 设置读取结束标记
        socket.shutdownInput();

        // 5.关闭流对象和socket,必须关闭
        inputStream.close();
        outputStream.close();
        socket.close();
        System.out.println("客户端 退出...");
    }
}

示例代码03-字符流

题目介绍
  1. 编写一个服务器端,和一个客户端
  2. 服务器端在 9999端口监听
  3. 客户端连接到服务器端,发送 "hello, server",并接收服务器端回发的 "hello client",再退出。
  4. 服务器端接收到 客户端发送的 信息,输出,并发送 "hello, client", 再退出。
示意图

image-20220302204929987.

服务端
// 服务端
public class SocketTCP03Server {
    public static void main(String[] args) throws IOException {
        // 思路
        // 1.在本机 的9999端口监听,等待连接
        //   细节:要求在本机没有其他服务在监听9999
        //   细节:这个 ServerSocket 可以通过 accept() 返回多个Socket[多个客户端连接服务器的并发]
        ServerSocket server = new ServerSocket(9999);
        System.out.println("服务端在9999端口监听,等待连接...");
        // 2.当没有客户端连接9999端口时,程序会阻塞,等待连接
        //   如果有客户端连接,则会返回Socket对象,程序继续执行
        Socket socket = server.accept();
        System.out.println("服务端 socket=" + socket.getClass());

        // 3.通过socket.getInputStream() 读取 客户端写入到数据通道的数据,并显示
        InputStream inputStream = socket.getInputStream();
        // 4.IO读取, 使用字符流 转换流
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
//        System.out.println(br.readLine());
        String line = "";
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        // 设置读取结束标记
        socket.shutdownInput();

        // 5.获取socket相关联的输出流
        OutputStream outputStream = socket.getOutputStream();
        // 使用字符输出流的方式回复信息
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
        bw.write("hello client 字符流");
        bw.newLine();
        // 需要手动刷新
        bw.flush();
        // 设置写入结束标记
        socket.shutdownOutput();


        // 6.关闭流和socket
        bw.close();
        br.close();
        socket.close();
        server.close();
        System.out.println("服务器 退出....");
    }
}
客户端
// 客户端
public class SocketTCP03Client {
    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.通过输出流,写入数据到 数据通道,使用字符流
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(outputStream));
        bw.write("hello server 字符流");
        // 插入一个换行符,表示写入的内容结束,注意,要求对方使用readLine()!!!且不能使用while循环!!
        bw.newLine();
        // 如果使用字符流,需要手动刷新,否则数据不会写入到数据通道
        bw.flush();
        // 设置写入结束标记
        socket.shutdownOutput();

        // 4.获取和socket相关联的输入流,读取数据(字符),并显示
        InputStream inputStream = socket.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String line = "";
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        // 设置读取结束标记
        socket.shutdownInput();

        // 5.关闭流对象和socket,必须关闭
        br.close();
        bw.close();
        socket.close();
        System.out.println("客户端 退出...");
    }
}

示例代码04-网络上传文件(upload)

题目介绍
  1. 编写一个服务器端,和一个客户端
  2. 服务器端在 8888端口监听
  3. 客户端连接到服务器端,发送 一张图片D:\\qie.png
  4. 服务器端接收到 客户端发送的 图片,保存到src下,并发送 "收到图片", 再退出。
  5. 客户端接收到 服务器端回发的 "收到图片",再退出。
示意图

image-20220304215038804.

服务端
// 服务端
public class TCPFileCopyServer {
    public static void main(String[] args) throws IOException {
        // 1.服务器 在本机监听 8888端口
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服务器 在8888端口监听,等待连接...");
        // 2.等待连接,程序阻塞
        Socket socket = serverSocket.accept();

        // 3.读取客户端发送的数据
        //   通过socket得到输入流
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        // 4.创建字节输出流
        String destFilePath = "src\\wb2.png";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFilePath));
        // 字节缓冲数组
        byte[] buff = new byte[1024];
        // 读取长度
        int readLen = 0;
        // 5.循环读取,边读边写 数据通道->磁盘
        while ((readLen = bis.read(buff)) != -1) {
            bos.write(buff, 0, readLen);
        }
        // 手动刷新
        bos.flush();
        // 设置读取结束标志
        socket.shutdownInput();

        // =====向客户端回复 "收到图片"
        // 6.通过socket得到输出流
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        // 写入信息
        bw.write("收到图片..");
        // 手动刷新
        bw.flush();
        // 写入结束标志
        socket.shutdownOutput();

        // 关闭资源
        bw.close();
        bos.close();
        bis.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务器 退出...");
    }
}
客户端
// 客户端
public class TCPFileCopyClient {
    public static void main(String[] args) throws IOException {
        // 1.客户端连接服务器 8888,得到Socket对象
        Socket socket = new Socket(InetAddress.getByName("192.168.184.193"), 8888);

        // 2.创建读取磁盘文件的输入流 磁盘->内存
        String filePath = "D:\\wb.png";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(filePath));

        // 3.通过socket获取到输出流,将数据写入到数据通道
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        // 缓冲数组
        byte[] buff = new byte[1024];
        // 读取长度
        int readLen = 0;
        // 循环读取,边读边写 磁盘->数据通道
        while ((readLen = bis.read(buff)) != -1) {
            bos.write(buff, 0, readLen);
        }
        // 手动刷新!!!
        bos.flush();
        // 设置写入结束标志
        socket.shutdownOutput();

        // =====接收从服务端回复的消息
        // 这里字节流 和 字符流都可以使用
        // 如何选择:
        // (1) 读取文本文件:字符流
        // (2) 读取二进制文件(图片、音乐):字节流
        //
        // 文本文件读取分析
        // 1.使用字节输入流读取数据,出现乱码
        // why?
        // (1)字节数组太小,UTF-8中一个汉字占3个字节(3byte) 1个字符(1char)
        // (2)导致读取数据时,只读取了汉字的部分字节(读取汉字不完整),出现乱码
        // buff = new byte[2];
        // InputStream inputStream = socket.getInputStream();
        // while ((readLen = inputStream.read(buff)) != -1) {
        //     System.out.println(new String(buff, 0, readLen));
        // }

        // 2.使用字符输入流读取数据,没有出现乱码(推荐)
        // 使用readLine方法读取字符时,不会 读取换行符!!
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 使用StringBuilder对象 来进行字符串的拼接
        StringBuilder res = new StringBuilder();
        String line = "";
        while ((line = br.readLine()) != null) {
            res.append(line).append("\r\n");
        }
        System.out.println(res);

        // 3.使用字符缓冲数组来读取信息,不会出现乱码问题,因为1个汉字就是1个字符
        //   不会出现只读取汉字的一部分的情况
        //  而且不会漏掉换行符,即换行符也会读取(但是readLine方法 不会读取换行符)
        // BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // char[] buf = new char[1024];
        // while ((readLen = br.read(buf)) != -1) {
        //     System.out.println(new String(buf, 0, readLen));
        // }
        socket.shutdownInput();

        //inputStream.close();
        bos.close();
        bis.close();
        socket.close();
        System.out.println("客户端退出..");
    }
}

netstat指令

基本介绍

  1. netstat -an:可以查看当前主机网络情况,包括端口监听情况和网络连接情况。
  2. netstat -an | more:可以分页显示。
  3. 要求在 dos 控制台下执行 win+R

​ ne

示意图

image-20220304213115832.

image-20220304213047087.

说明

  1. established:已建立连接
  2. listening:监听 表示某个端口在监听
  3. 如果有一个外部程序(客户端)连接到该端口,就会显示一条连接信息。
  4. 可以输入 ctrl+C 退出指令

TCP网络通讯不为人知的秘密

基本介绍

  1. 当客户端连接到服务端后,实际上 客户端也是通过一个端口和服务端进行通讯 的,这个端口是TCP/IP来分配的,是不确定的,是随机的。

示意图

image-20220304215017888.

UDP网络通信编程【了解】

基本介绍

  1. DatagramSocketDatagramPacket(数据包/数据报) 实现了基于 UDP 协议网络程序。
  2. UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证UDP数据报一定能够安全送到目的地,也不能确定什么时候可以抵达。
  3. DatagramPacket 对象封装了UDP数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号。
  4. UDP协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接

基本流程

  1. 核心的两个类/对象:DatagramSocketDatagramPacket.
  2. 建立发送端,接收端。
  3. 建立数据包。
  4. 调用 DatagramSocket 的发送、接收方法。
  5. 关闭DatagramSocket.

示意图

image-20220304225047014.

示例代码-UDP通信

receive 接收.

send 发送.

接收端A
// 接收端A
public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        // 1.创建一个 DatagramSocket对象, 准备在 9999端口接收数据
        DatagramSocket socket = new DatagramSocket(9999);
        // 2.构建一个 DatagramPacket对象,准备接收数据
        //   在前面讲解UDP 协议时,说过一个数据包最大 64K (即 64*1024 byte)
        byte[] buff = new byte[1024];
        DatagramPacket packet = new DatagramPacket(buff, buff.length);
        // 3.调用 接收方法,将通过网络传输的 DatagramPacket对象
        //   填充到 packet对象
        // 提示:当有数据包/报 发送到 本机的9999端口时,就会接收到数据 到packet
        //      如果没有数据包/报 发送到 本机的9999端口时,就会阻塞等待.
        System.out.println("接收端A 等待接收数据...");
        socket.receive(packet);

        // 4.把packet 进行拆包,取出数据,并显示
        // 实际接收到的数据字节长度
        int length = packet.getLength();
        // 接收到的数据
        byte[] data = packet.getData();
        // 构建数据 不要忘了 offset:0
        String s = new String(data, 0, length);
        System.out.println(s);

        // =====回复信息给B端
        byte[] message = "好的 明天见".getBytes();
        packet =
                new DatagramPacket(message, 0, message.length, InetAddress.getByName("192.168.240.1"), 8888);
        socket.send(packet);

        // 5.关闭资源
        socket.close();
        System.out.println("A端 退出..");
    }
}
发送端B
// 发送端B
public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        // 1.创建 DatagramSocket对象,准备在8888端口 接收数据
        DatagramSocket socket = new DatagramSocket(8888);
        // 2.将需要发送的数据,封装到 DatagramPacket对象中
        byte[] data = "hello 明天吃火锅".getBytes();
        // 说明:封装的 DatagramPacket对象
        // data内容:字节数组
        // offset(起始位置):0
        // data.length(偏移长度)
        // 主机(ip) 端口(port)
        DatagramPacket packet =
                new DatagramPacket(data, 0, data.length, InetAddress.getByName("192.168.240.1"), 9999);
        // 3.发送数据
        socket.send(packet);

        // =====接收从A端回复的信息
        // (1)
        byte[] buff = new byte[1024];
        packet = new DatagramPacket(buff, buff.length);
        // (2)
        socket.receive(packet);
        // (3)
        int length = packet.getLength();
        byte[] packetData = packet.getData();
        System.out.println(new String(packetData, 0, length));

        // 4.关闭资源
        socket.close();
        System.out.println("B端 退出..");
    }
}

多用户即时通信系统

项目开发流程

需求分析 --> 设计阶段 --> 编码实现 --> 测试阶段 --> 实施阶段 --> 维护阶段

  • 示意图

image-20220305130333543.

多用户通信需求分析

  1. 用户登录
  2. 拉取在线用户列表
  3. 无异常退出(客户端、服务端)
  4. 私聊
  5. 群聊
  6. 发文件
  7. 服务器推送新闻

通信系统整体分析

功能实现

用户登录

common 共有的、共享的.

content 内容.

..........

反射(reflection)

一个需求引出反射

快速入门

  1. 根据配置文件 re.properties 指定信息,创建对象并调用方法

    classFullPath=com.lemon.Cat

    method=hi

    思考:使用现有技术,你能做到吗? 不行了

  2. 通过外部文件配置,在不修改源码情况下,来控制程序,也符合设计模式的ocp原则(开闭原则:不修改源码,扩展功能)

  3. 反射是框架的灵魂,框架的基石.

示例代码

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

        // 传统方式 new对象-> 调用方法
        // Cat cat = new Cat();
        // cat.hi(); ====> cat.cry(); 修改源码

        // 根据配置文件 re.properties 指定信息,创建对象并调用方法
        // 1.使用Properties 类,可以读写配置文件
        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream("src\\re.properties"));
            String path = properties.getProperty("classFullPath");
            String methodName = properties.getProperty("method");
            System.out.println(path + " " + methodName);

            // 2.创建对象,传统的方法,行不通
            // new path()

            // 3.使用反射机制解决
            // (1) 加载类,返回Class类型的对象
            Class<?> cls = Class.forName(path);
            // (2) 通过 cls对象得到你加载的类 com.lemon.Cat 的对象实例
            Object o = cls.newInstance();
            System.out.println("o 的运行类型=" + o.getClass());// 运行类型
            // (3) 通过 cls 得到你加载的类 com.lemon.Cat 的 methodName 的方法对象
            //     即:在反射中,可以把方法视为对象 (万物皆对象)
            Method method = cls.getMethod(methodName);
            // (4) 通过 method 调用方法:即通过方法对象来实现调用方法
            //     传统方法 对象.方法() , 反射机制 方法.invoke(对象)
            // invoke:调用
            System.out.println("===========");
            method.invoke(o);
        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

反射机制

java Reflection

  1. 反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会使用。

  2. 加载完类之后,在中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个Class对象就像一面镜子,透过这个镜子看到类的结构,所以,形象称之为:反射.

    p 对象 --》 类型 Person类

    对象cls --> 类型 Class类(本质就是一个类,只是这个类名有点特别)

java反射机制原理示意图

image-20220312140233083.

java反射机制可以完成

  1. 在运行时判断任意一个对象所属的类.
  2. 在运行时构造(创建)任意一个类的对象.
  3. 在运行时得到任意一个类所具有的成员变量和方法.
  4. 在运行时调用任意一个对象的成员变量和方法.
  5. 生成动态代理.

反射相关的主要类:

  1. java.lang.Class:代表一个类,Class对象表示某个类加载后在堆中的对象。
  2. java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法。
  3. java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量。
  4. java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器。
示例代码
public class Reflection01 {
    public static void main(String[] args) {
        // 1.使用Properties 类,可以读写配置文件
        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream("src\\re.properties"));
            String path = properties.getProperty("classFullPath");
            String methodName = properties.getProperty("method");
            System.out.println(path + " " + methodName);

            // 2.创建对象,传统的方法,行不通
            // new path()

            // 3.使用反射机制解决
            // (1) 加载类,返回Class类型的对象
            Class<?> cls = Class.forName(path);
            // (2) 通过 cls对象得到你加载的类 com.lemon.Cat 的对象实例
            Object o = cls.newInstance();
            System.out.println("o 的运行类型=" + o.getClass());// 运行类型
            // (3) 通过 cls 得到你加载的类 com.lemon.Cat 的 methodName 的方法对象
            //     即:在反射中,可以把方法视为对象 (万物皆对象)
            Method method = cls.getMethod(methodName);
            // (4) 通过 method 调用方法:即通过方法对象来实现调用方法
            //     传统方法 对象.方法() , 反射机制 方法.invoke(对象)
            // invoke:调用
            System.out.println("===========");
            method.invoke(o);


            // java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量。
            // 得到name字段
            // getField() 不能得到私有的属性
            Field ageField = cls.getField("age");
            // 传统写法 对象.成员变量 , 反射:成员变量对象.get(对象)
            Object content = ageField.get(o);
            System.out.println(content);

            // java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器
            // () 中可以指定构造器的参数类型, 返回无参构造器
            Constructor<?> constructor1 = cls.getConstructor();
            System.out.println(constructor1);
            // String.class 就是String类的class对象
            Constructor<?> constructor2 = cls.getConstructor(String.class);
            System.out.println(constructor2);
            

        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException | NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}

反射优点和缺点

  1. 优点:可以动态的创建和使用对象(可是框架底层核心),使用灵活,没有反射机制,框架技术就失去底层支撑。
  2. 缺点:使用反射基本是解释执行,对执行速度有影响

反射调用优化-关闭访问检查

Accessible 可使用的,可进入的.

  1. MethodFieldConstructor对象都有setAccessible() 方法.
  2. setAccessible作用是启动和禁用访问安全检查的开关。
  3. 参数值为true表示 反射的对象在使用时取消访问检查,提高反射效率。参数值为false则表示反射的对象执行访问检查。
示例代码
// 测试反射调用的性能,和优化方案
public class Reflection02 {
    public static void main(String[] args) throws Exception {
        m1();
        m2();
        m3();
    }

    // 传统方法来调用hi
    public static void m1() {
        Cat cat = new Cat();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            cat.hi();
        }
        long end = System.currentTimeMillis();
        System.out.println("m1() 耗时=" + (end - start));
    }

    // 反射机制调用方法hi
    public static void m2() throws Exception {
        Class<?> cls = Class.forName("com.lemon.Cat");
        Method method = cls.getMethod("hi");
        Object o = cls.newInstance();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            method.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("m2() 耗时=" + (end - start));
    }

    // 反射机制调用优化 + 关闭访问检查
    public static void m3() throws Exception {
        Class<?> cls = Class.forName("com.lemon.Cat");
        Method method = cls.getMethod("hi");
        Object o = cls.newInstance();
        // 反射调用方法时,取消访问检查
        method.setAccessible(true);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 900000000; i++) {
            method.invoke(o);
        }
        long end = System.currentTimeMillis();
        System.out.println("m3() 耗时=" + (end - start));
    }
}

Class类

基本介绍

  1. Class也是类,因此也继承Object类。
  2. Class类对象不是new出来的,而是系统创建的。
  3. 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  4. 每个类的实例都会记得自己是由哪个 Class 实例所生成。
  5. 通过Class对象 可以完整地得到一个类的完整结构,通过一系列API
  6. Class对象存放在堆中。
  7. 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括 方法代码,变量名,方法名,访问权限等等)
类的示意图

image-20220309164825782.

示例代码
// 对 Class类特点的梳理
public class Class01 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        // Class类类图
        // 1.Class也是类,因此也继承Object类
        // Class
        // 2.Class类对象不是new出来的,而是系统创建的
        // (1) 传统new对象
        /*
            ClassLoader类
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
         */
        //Cat cat = new Cat();
        // (2) 反射方式
        // 刚才没有debug到 ClassLoader类的 loadClass,原因是,我没有注销 Cat cat = new Cat();
        /*
            ClassLoader类,仍然是通过 ClassLoader类加载Cat类的 Class对象
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
         */
        Class<?> cls = Class.forName("com.lemon.Cat");
        Object o = cls.newInstance();
        // 3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
        Class<?> cls2 = Class.forName("com.lemon.Cat");
        System.out.println(cls.hashCode());
        System.out.println(cls2.hashCode());
        Class<?> cls3 = Class.forName("com.lemon.Dog");
        System.out.println(cls3.hashCode());
        
    }
}

Class类的常用方法

方法名 功能说明
static Class forName(String name) 返回指定类名 name 的Class 对象
Object newInstance() 调用无参构造函数,返回该 Class 对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型等) 名称
Class getSuperclass() 返回当前Class对象的父类的Class对象
Class[] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String name, Class...paramTypes) 返回一个Method对象,此对象的形参类型为paramType
示例代码
// 演示Class类的常用方法
public class Class02 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchFieldException {
        String classFullPath = "com.lemon.Car";
        // 1.获取到Car类 对应的 Class对象
        // <?> 表示不确定的Java类型
        Class<?> cls = Class.forName(classFullPath);
        // 2.输出cls
        // 显示cls对象,是哪个类的Class对象 com.lemon.Car
        // 此时这个cls并不是真正的Car对象(运行类型是Class),不能强转成Car对象
        System.out.println(cls);
        // 输出cls运行类型com.lemon.Class
        System.out.println(cls.getClass());

        // 3.得到包名
        System.out.println(cls.getPackage().getName());// 包名
        // 4.得到全类名
        System.out.println(cls.getName());
        // 5.通过cls创建对象实例 这是一个真正的Car对象,可以进行强转
        Object o = cls.newInstance();
        Car car = (Car) o;
        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 field : fields) {
            System.out.println(field.getName());// 名称
        }

        // 异常信息
        // ClassNotFoundException 类没有找到异常
        // IllegalAccessException 非法访问异常
        // InstantiationException 实例化异常
        // NoSuchFieldException 没有这样的字段异常(无此字段异常)
    }
}

获取Class类对象

  1. 前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName() 获取,可能抛出ClassNotFoundException,实例:Class cls1 = Class.forName("java.lang.Cat");

    应用场景:多用于配置文件,读取类全路径,加载类。

  2. 前提:若已知具体的类,通过类的class 获取(小写class),该方式最为安全可靠,程序性能最高。

    实例:Class cls2 = Cat.class;

    应用场景:多用于参数传递,比如通过反射得到对应构造器对象。

  3. 前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象。

    实例:Class clazz1 = 对象.getClass();// 运行类型

    应用场景:通过创建好的对象,获取Class对象。

  4. 其他方式 通过类加载器来获取类的Class对象:

    ClassLoader clazzLoader = 对象.getClass().getClassLoader();

    Class clazz3 = clazzLoader.loadClass("类的全类名");

  5. 基本数据类型(int、char、boolean、float、double、byte、long、short) 按如下方式得到Class类对象:

    Class cls = 基本数据类型.class;

  6. 基本数据类型对应的包装类,可以通过 .TYPE 得到Class对象

    Class cls = 包装类.TYPE;

示例代码
// 演示得到Class对象的6种方式
public class GetClass {
    public static void main(String[] args) throws Exception {
        // 1.Class.forName()
        String classFullPath = "com.lemon.Car";
        Class<?> cls1 = Class.forName(classFullPath);
        System.out.println(cls1);

        // 2.类名.class 应用场景:参数传递
        Class<Car> cls2 = Car.class;
        System.out.println(cls2);

        // 3. 对象.getClass(), 应用场景:有对象实例
        Car car = new Car();
        Class<? extends Car> cls3 = car.getClass();
        System.out.println(cls3);

        // 4.通过类加载器【4种】来获取到类的Class对象
        // (1)先得到类加载器 car
        ClassLoader classLoader = car.getClass().getClassLoader();
        // (2)通过类加载器得到Class对象
        Class<?> cls5 = classLoader.loadClass(classFullPath);
        System.out.println(cls5);

        // cls1, cls2, cls3, cls5 其实是同一个对象
        System.out.println(cls1.hashCode());
        System.out.println(cls2.hashCode());
        System.out.println(cls3.hashCode());
        System.out.println(cls5.hashCode());

        // 5.基本数据类型
        Class<Integer> integerClass = int.class;
        Class<Character> characterClass = char.class;
        Class<Boolean> booleanClass = boolean.class;
        System.out.println(integerClass);// int

        // 6.包装类
        Class<Integer> integerClass1 = Integer.TYPE;
        Class<Character> characterClass1 = Character.TYPE;
        System.out.println(integerClass1);

        System.out.println(integerClass.hashCode());
        System.out.println(integerClass1.hashCode());
    }
}

哪些类型有Class对象

  • 如下类型有Class对象
    1. 外部类,成员内部类,静态内部类,局部内部类,匿名内部类。
    2. interface:接口
    3. 数组
    4. enum:枚举
    5. annotation:注解
    6. 基本数据类型
    7. void
示例代码
// 演示哪些类型有Class对象
public class AllTypeClass {
    public static void main(String[] args) {
        // 1.外部类
        Class<String> cls1 = String.class;
        // 2.接口
        Class<Serializable> cls2 = Serializable.class;
        // 3.数组
        Class<int[]> cls3 = int[].class;
        // 二维数组
        Class<Integer[][]> cls4 = Integer[][].class;
        // 4.注解
        Class<Deprecated> cls5 = Deprecated.class;
        // 5.枚举
        Class<Thread.State> cls6 = Thread.State.class;
        // 6.基本数据类型
        // 7.包装类
        // 8.void
        Class<Void> cls7 = void.class;
        // 9.Class
        Class<Class> cls8 = Class.class;

        System.out.println(cls4);
        
    }
}

类加载

基本说明

反射机制是java实现动态语言的关键,也就是通过反射实现类动态加载。

  1. 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强。
  2. 动态加载:运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性。

类加载时机

  1. 当创建对象时(new) // 静态加载
  2. 当子类被加载时,父类也加载 // 静态加载
  3. 调用类中的静态成员时 // 静态加载
  4. 通过反射 // 动态加载

类加载过程图

image-20220312145552805.

类加载各阶段完成任务

image-20220312145632707.

1.加载阶段

  • JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class文件、也可能是jar包,甚至网络) 转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class 对象。

2.链接阶段

2.1验证
  1. 目的是为了确保 Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  2. 包括:文件格式验证(是否以魔数 oxcafebabe开头)。元数据验证、字节码验证和符号引用验证。
  3. 可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载时间。
2.2准备
  1. JVM 会在该阶段对静态变量,分配内存并默认初始化(对应数据类型的默认初始值,如00Lnullfalse等)。这些变量所使用的内存都将在方法区中进行分配。
  2. 示例代码
class A {
    // 属性-成员变量-字段
    // 分析 类加载的链接阶段-准备 属性如何处理
    // 1.n1 是实例属性,不是静态变量,因此在准备阶段,是不会分配内存的
    // 2.n2 是静态变量,分配内存 n2 此时默认初始化为0,而不是20(初始化阶段才为20)
    // 3.n3 是static final类型的,是常量,它和静态变量(类变量)不一样,一旦赋值就不会改变 即n3=30
    public int n1 = 10;
    public static int n2 = 20;
    public static final int n3 = 30;
}
2.3解析
  1. 虚拟机将常量池内的符号引用替换为直接引用的过程。

3.initialization(初始化)

  1. 到初始化阶段,才真正开始执行类中定义的Java代码,此阶段是执行<clinit>() 方法的过程。
  2. <clinit>() 方法是由编译器按语句在源文件中出现的顺序,依次按顺序自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。
  3. 虚拟机会保证一个类的clinit() 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类地 clinit() 方法,其他线程都需要阻塞等待,直到活动线程执行clinit() 方法完毕。
  4. 示例代码
// 初始化
public class ClassLoad03 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 分析
        // 1.加载B类,并生成B的class对象
        // 2.链接 num = 0
        // 3.初始化阶段
        // 依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并
        /*
            clinit() {
                System.out.println("B 的静态代码块被执行~");
                num = 300;
                num = 100
            }
            合并后:num = 100;
         */
        // new B();// 会执行构造器
        // 如果直接使用类的静态属性 也会导致类的加载
        // System.out.println(B.num);// 不会执行构造器

        // 看看加载类的时候,是有同步机制控制
        /*
            protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
            {
                // 正因为有这个机制,才能保证某个类在内存中,只有一份Class对象
                synchronized (getClassLoadingLock(name)) {
                    // .......
                }
            }
         */
        B b = new B();
    }
}

class B {
    static {
        System.out.println("B 的静态代码块被执行~");
        num = 300;
    }

    static int num = 100;

    public B() {
        System.out.println("B() 的构造器被执行~");
    }
}

通过反射获取类的结构信息

第一组:java.lang.Class类

  1. getName:获取全类名
  2. getSimpleName:获取简单类名
  3. getFields:获取所有public修饰的属性,包含本类以及父类的
  4. getDeclaredFields:获取本类中所有属性,包括子类非public,不包括任何父类属性
  5. getMethods:获取所有public修饰的方法,包含本类以及父类的(不限于直接父类)
  6. getDeclaredMethods:获取本类中所有方法,包括子类非public,不包括任何父类方法
  7. getConstructors:获取本类所有public修饰的构造器,不包括父类的
  8. getDeclaredConstructors:获取本类中所有构造器, 包括非public的
  9. getPackage:以Package形式返回包信息
  10. getSuperclass:以Class形式返回父类信息
  11. getInterfaces:以Class[] 形式返回接口信息
  12. getAnnotations:以Annotation[] 形式返回注解信息
  • 示例代码
// 演示如何通过反射获取类的结构信息
public class ReflectionUtils {

    @Test
    public void api01() throws Exception {
        // 得到Class对象
        Class<?> personCls = Class.forName("com.lemon.reflection.Person");

        //1. getName:获取全类名
        System.out.println(personCls.getName());// com.lemon.reflection.Person
        //2. getSimpleName:获取简单类名
        System.out.println(personCls.getSimpleName());// Person
        //3. getFields:获取所有public修饰的属性,包含本类以及父类的
        Field[] fields = personCls.getFields();
        for (Field field : fields) {// 增强for
            System.out.println(field.getName());
        }
        //4. getDeclaredFields:获取本类中所有属性, 包括非public,不包括父类属性
        System.out.println("====");
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field field : declaredFields) {
            System.out.println(field.getName());
        }
        //5. getMethods:获取所有public修饰的方法,包含本类以及父类的(不限于直接父类)
        System.out.println("====");
        Method[] methods = personCls.getMethods();
        for (Method method : methods) {
            System.out.println(method.getName());
        }
        //6. getDeclaredMethods:获取本类中所有方法,包括非public,不包括父类方法
        System.out.println("=====");
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println(declaredMethod.getName());
        }
        //7. getConstructors:获取本类所有public修饰的构造器
        System.out.println("===");
        Constructor<?>[] constructors = personCls.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println(constructor.getName());
        }
        //8. getDeclaredConstructors:获取本类中所有构造器, 包括非public的
        System.out.println("======");
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println(declaredConstructor.getName());
        }
        //9. getPackage:以Package形式返回包信息
        System.out.println(personCls.getPackage());// package com.lemon.reflection
        //10. getSuperclass:以Class形式返回父类信息
        Class<?> superclass = personCls.getSuperclass();
        System.out.println(superclass);
        //11. getInterfaces:以Class[] 形式返回接口信息
        Class<?>[] interfaces = personCls.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println(anInterface);
        }
        //12. getAnnotations:以Annotation[] 形式返回注解信息
        Annotation[] annotations = personCls.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation);
        }

    }
}
interface IA {}
interface IB {}
class A {
    public A() {}
    public String hobby;
    protected String color;
    public void hi1(){}
    protected void hi2() {}
}
@Deprecated
class Person extends A implements IA,IB {
    // 属性
    public String name;
    protected int age;
    String job;
    private double sal;
    // 构造器
    public Person() {}
    public Person(String name) {}
    private Person(int age) {}
    // 方法
    public void m1() {}
    protected void m2() {}
    void m3() {}
    private void m4() {}
}

第二组:java.lang.reflect.Field类

  1. getModifiers:以int形式返回修饰符【说明:默认修饰符 是0,public 是1,private 是 2,protected 是 4,static 是 8,final 是 16】public static = 1+8 = 9
  2. getType:以Class形式返回属性类型
  3. getName:返回属性名
@Test
public void api02() throws Exception {
    // 得到Class对象
    Class<?> personCls = Class.forName("com.lemon.reflection.Person");
    //4. getDeclaredFields:获取本类中所有属性, 包括非public,不包括父类属性
    Field[] declaredFields = personCls.getDeclaredFields();
    for (Field field : declaredFields) {
        System.out.println("本类中所有属性 " + field.getName()
                           + " 该属性的修饰符值=" + field.getModifiers()
                           + " 该属性的类型" + field.getType());
    }
}

第三组:java.lang.reflect.Method类

  1. getModifiers:以int形式返回修饰符【说明:默认修饰符 是0,public 是1,private 是 2,protected 是 4,static 是 8,final 是 16】public static = 1+8 = 9
  2. getReturnType:以Class形式获取 返回类型
  3. getName:返回方法名
  4. getParameterTypes:以Class[] 返回参数类型数组
@Test
public void api03() throws Exception {
    // 得到Class对象
    Class<?> personCls = Class.forName("com.lemon.reflection.Person");
    //6. getDeclaredMethods:获取本类中所有方法,包括非public,不包括父类方法
    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);
        }
    }
}

第四组:java.lang.reflect.Constructor类

  1. getModifiers:以int形式返回修饰符
  2. getName:返回构造器名(全类名)
  3. getParameterTypes:以Class[] 返回参数类型数组
@Test
    public void api04() throws Exception {
        // 得到Class对象
        Class<?> personCls = Class.forName("com.lemon.reflection.Person");
        //8. getDeclaredConstructors:获取本类中所有构造器, 包括非public的
        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);
            }
        }
    }

反射爆破

创建实例

  1. 方式一:调用类中的public修饰的无参构造器
  2. 方式二:调用类中指定构造器
  3. Class类相关方法
    • newInstance:调用类中的无参构造器,获取对应类的对象。
    • getConstructor(Class... clazz):根据参数列表,获取对应的public构造器对象。
      • Class... clazz:可变参数(可以传入任意个数的参数,一个不填也可以)
    • getDeclaredConstructor(Class... clazz):根据参数列表,获取对应的所有构造器对象。
  4. Constructor类相关方法
    • setAccessible:暴破,只要是使用private的构造器、属性、方法,(修改值或读取值)就需要暴破,但如果只是获取则不用使用暴破,调用带有Declared的对应方法即可。
    • newInstance(Object... obj):调用构造器

示例代码

public class ReflectCreateInstance {
    public static void main(String[] args) throws Exception {
        // 1.先获取到User类的Class对象
        Class<?> userClass = Class.forName("com.lemon.reflection.User");
        // 2.通过public的无参构造器创建实例
        Object o = userClass.newInstance();
        System.out.println(o);
        // 3.通过public的有参构造器创建实例
        /*
            constructor 对象就是
            public User(String name) {// 有参构造器 public
                this.name = name;
            }
         */
        // 3.1 先得到对应构造器
        Constructor<?> constructor = userClass.getConstructor(String.class);
        // initargs:初始化参数
        // 3.2 创建实例,并传入实参
        Object sunnyYang = constructor.newInstance("SunnyYang");
        System.out.println(sunnyYang);
        // 4.通过非public的有参构造器创建实例
        // 4.1 得到private构造器对象
        Constructor<?> constructor1 = userClass.getDeclaredConstructor(String.class, int.class);
        // 4.2 创建实例
        // IllegalAccessException: 非法访问异常
        // 暴破【暴力破解】,使用反射可以访问private构造器,反射面前都是纸老虎
        constructor1.setAccessible(true);
        Object monkey = constructor1.newInstance("monkey", 12);
        System.out.println(monkey);
    }
}

class User {
    private int age = 10;
    private String name = "软柠柠";

    public User() {// 无参构造器 public
    }

    public User(String name) {// 有参构造器 public
        this.name = name;
    }

    private User(String name, int age) {// 有参构造器 private
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

操作属性

  1. 根据属性名获取Field对象

    Field f = clazz对象.getDeclaredField(属性名);

  2. 暴破:f.setAccessible(true);// f 是Field对象

  3. 访问

    f.set(o, 值); // o 表示对象

    f.get(o); // o 表示对象

  4. 注意:如果是静态属性,则set和get中的参数o,可以写成null

示例代码

public class ReflectAccessProperty {
    public static void main(String[] args) throws Exception {
        // 1.得到Student类对应的Class对象
        Class<?> stuClass = Class.forName("com.lemon.reflection.Student");
        // 2.创建对象
        // o 的运行类型是 Student
        Object o = stuClass.newInstance();
        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, "软柠柠");
        // 因为name是static属性,因此 o 也可以写成null
        name.set(null, "软柠柠吖");
        System.out.println(o);
        System.out.println(name.get(o));// 获取属性值
        System.out.println(name.get(null));// 获取属性值 要求name是static

    }
}

class Student {
    public int age;
    private static String name;

    public Student() {
    }

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                " name=" + name +
                '}';
    }
}

操作方法

  1. 根据方法名和参数列表获取Method方法对象:

    Method m = clazz.getDeclaredMethod(方法名, XX.class);

  2. 获取对象

    Object o = clazz.newInstance();

  3. 暴破:m.setAccessible(true);

  4. 访问:Object returnValue = m.invoke(o, 实参列表);// o 就是对象

  5. 注意:如果是静态方法,则invoke的参数o, 可以写成null.

示例代码

public class ReflectAccessMethod {
    public static void main(String[] args) throws Exception {
        // 1.得到Boss类对应的Class对象
        Class<?> bossClass = Class.forName("com.lemon.reflection.Boss");
        // 2.创建对象
        Object o = bossClass.getConstructor().newInstance();
        // 3.调用public的 hi方法
        // Method hi = bossClass.getMethod("hi", String.class);
        // 3.1得到hi方法对象
        Method hi = bossClass.getDeclaredMethod("hi", String.class);
        // 3.2调用
        hi.invoke(o, "软柠柠");
        // 4.调用 private static 方法
        // 4.1得到 say 方法对象
        Method say = bossClass.getDeclaredMethod("say", int.class, String.class, char.class);
        // 4.2调用 因为say方法是private,所有需要暴破,原理和前面的构造器和属性一样
        say.setAccessible(true);
        Object str = say.invoke(o, 100, "里斯", '男');
        System.out.println(str);
        // 4.3 因为say 方法是static的,所以对象可以传入null
        Object str2 = say.invoke(null, 100, "里斯", '男');
        System.out.println(str2);

        // 5.在反射中,如果方法有返回值,统一返回Object,但是它运行类型和方法定义的返回类型一致
        Object returnValue = say.invoke(null, 300, "王小五", '女');
        System.out.println(returnValue);
        System.out.println(returnValue.getClass());

    }
}

class Boss {
    public int age;
    private static String name;

    public Boss() {
    }

    private static String say(int n, String s, char c) {// 静态方法 private
        return n + " " + s + " " + c;
    }

    public void hi(String s) { // 普通方法 public
        System.out.println("hi " + s);
    }
}

健壮性

  1. 数组:

    • 数组不为 null

    • 数组不为 {}

    if (arr == null || arr.length == 0) { // 顺序不能反,先判断不为null,在判断是否为空
        return;
    }
    

idea小技巧

  1. 可以用 ideastructure快速定位方法
  2. Alt+3Alt+F7 快速查看是谁调用方法.

Typora小技巧

1.页内链接

  1. # 开头接标题(不管标题是几级,这里只用一个#

  2. 按住ctrl点击跳转

[typora](#页内链接)

### 页内链接

2.链接外部文件

[typora](外部文件名)
// 不需要#

开发技巧

按照名字排序

最终版(最简洁)要学习,借鉴.

按照出生年月日排序

最终版(最简洁)要学习,借鉴.

数据结构

冒泡排序(bubbleSort)

public void bubble(int[] arr) {
    int temp = 0;
    for (int i = 0; i < arr.length - 1; i++) {// 外层循环i
        for (int j = 0; j < arr.length - 1 - i; j++) {// 内层循环j
            // 下面用的都是内层的 j, 注意!注意!,不能用i !!
            // if (arr[j] < arr[j + 1]) { 从大到"小"排序 "小" -对应- "<"
            if (arr[j] > arr[j + 1]) {// 从小到"大"排序  "大" -对应- ">"
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

小问题

字符串长度是方法

  • str.length(): 这是一个方法

数组长度是属性

  • arr.length: 这是一个属性

对象引用 与 null 之间用 "==" 判断即可

  • 对象引用 与 null之间用 == 判断,无法用equals方法判断。原因如下
    1. null没有equals方法.所以不能用 (null).equals(str).
    2. 对象为null时, 调用equals方法时会产生NullPointerException空指针异常, 所以也不能用str.equals(null).

索引 从0开始 类似于"指针" 有范围就前闭后开

  • java中索引都是从0开始的,类似于"指针".
  • 遇到范围时,就前闭后开.

x号位 、第x个元素的索引是 x-1

  • 我们平时说的几号位,第几个元素 在程序中并不是直接对应索引index 而是 index-1.
  • 因为程序中的索引是从 0 开始的。
  • 例如: 第1个位置,对应的索引是 0;第6个位置,对应的索引是 5.

不等于 !=

!=:叹号和等号之间没有空格哦!!

posted @ 2022-10-23 17:44  软柠柠吖  阅读(25)  评论(0编辑  收藏  举报