** IDEA快捷键 **

ALT + Ins 快速创建Javabean

CTRL + P 查看方法的具体参数

ALT + ENTER 选择 Extract Method 快速生成方法

CTRL + ALT + L 格式化代码

CTRL + B 查看方法详细

CTRL + ALT + V 生成生成接收返回值的变量名

CTRL + ALT + T 将选中的代码以某种方式包裹起来(如try...catch

Java基础语法

注释

注释内容不会参与编译和运行,仅仅是对代码的解释说明

注释概念

  • 注释是在程序指定位置添加的说明性信息
  • 简单理解为:对代码的一种解释,方便阅读

注释分类

单行注释

1
// 注释信息

多行注释

1
/* 注释信息 */

文档注释

1
/** 注释信息 **/

关键字

关键字:被java赋予了特定涵义的英文单词

关键字特点

  • 关键字字母全部小写
  • 常用的代码编辑器,针对关键字有特殊的颜色标记,非常直观

class:用于创建/定义一个类,后面跟随类目。类是Java最基本的组成单元

⭐字面量⭐

数据在程序中的书写格式

字面量类型

字面量类型 说明 举例
整数类型 不带小数点的数字 666,-666
小数类型 带小数点的数字 3.1415,-3.1415
字符串类型 用双引号括起来的内容 “123”,”ABC”,”你好”
字符类型 用单引号括起来的内容,内容只能有一个 ‘A’,’0’,’嗨‘
布尔类型 布尔值,表示真、假 只有两个值:true,false
空类型 一个特殊的值,空值 null

拓展——特殊字符

\t 制表符

在打印的时候,把前面字符串的长度补齐到8,或者8的整数倍。最少补1个空格,最多补8个空格

1
2
3
4
5
6
7
public class HelloWorld{

public static void main(String[] args) {
// 补上5个空格
System.out.println("abc" + '\t');
}
}

作用:是文本输出时,更加美观、整齐

⭐变量⭐

变量:在程序的执行过程中,其值可能发生改变的量(数据)

1
// 数据类型 变量名 = 数据值;

数据类型:限定了变量能存储数据的类型

变量名:存储空间的名称

数据值:真正存在变量中的数据

1
2
3
4
5
6
7
8
9
10
// 定义变量
// 数据类型 变量名 = 数据值;
int a = 10;
System.out.println(a); // 10
// 变量参与计算
int b = 20;
System.out.println(a + b); // 30
// 修改记录的值
a = -10;
System.out.println(a); // -10
数据类型 关键字
整数 int
浮点数 double

变量的注意事项

  • 只能存一个值

  • 变量名不允许重复定义

  • 一条语句可以定义多个变量

    1
    2
    3
    // 在一条语句中可以定义多个变量
    int d = 100, e = 200, f = 300;
    System.out.println(d + e + f); // 600
  • 变量在使用之前一定要赋值

  • 变量的作用域范围

数据

不同进制在代码中的表现形式

二进制:由 0 和 1 组成,代码中以 0b 开头

十进制:由 0 ~ 9 组成,前面不添加前缀

八进制:由 0 ~ 7 组成,代码中以 0 开头

十六进制:由 0 ~ 9 以及 a ~ f 组成,代码中以 0x 开头

1
2
3
4
5
6
7
8
public class data{
public static void main(String[] args) {
System.out.println(15); // 十进制
System.out.println(015); // 八进制
System.out.println(0b1101); // 二进制
System.out.println(0x19f); // 十六进制
}
}

在计算机中,任意数据都是以二进制的形式来存储的

⭐数据类型⭐

基本数据类型

数据类型 关键字 范围 内存占用
整数 byte -128 ~ 127 1
short -32768 ~ 32767 2
int -2147483648 ~ 2147483647 (10位数) 4
long -9223372036854774808 ~ 9223372036854774807 (19位数) 8
浮点数 float -3.40E + 38 ~ 3.40E + 38 4
double -1.79E + 308 ~ 1.79E + 308 8
字符 char 0 ~ 65535 2
布尔 boolean true,false 1

注意:如果要定义long类型的变量,要在数据后面加一个 L 作为后缀(大小写都可以)

1
long num01 = 99999L

注意:如果要定义float类型的变量,要在数据后面加一个 F 作为后缀(大小写都可以)

1
float num02 = 10.1F;

整数和小数取值范围大小关系

double > float > long > int > short > byte

标识符

标识符:就是给类、方法、变量起的名字

要求

  • 由数字、字母、下划线 (_) 、美元符号 ($)组成
  • 不能以数字开头
  • 不能是关键字
  • 区分大小写

驼峰命名法

  • 小驼峰命名法

适用于方法、变量

例如:name、firstName

  • 大驼峰命名法

适用于类名

例如:Student,MyCode

⭐键盘录入⭐

Java中有一个类:Scanner,这个类就可以接收键盘输入的数字

使用方法

  1. 导包 — Scanner这个类在哪
1
import java.util.Scanner;
  1. 创建对象 — 表示要开始使用Scanner这个类了
1
Scanner sc = new Scanner(System.in)
  1. 接受数据 — 正式开始工作
1
int i = sc.nextInt();

运算符

运算符:对字面量或者变量进行操作的符号

表达式:用运算符把字面量或者变量连接起来,符合Java语法的式子就可以称为表达式

举例

1
2
3
int a = 10;
int b = 20;
int c = a + b;

算术运算符

注意:在代码中,如果有小数参与计算,可能会丢失精度

“+” 操作的三种情况

  • 数字相加
  • 字符串相加
  • 字符相加

数字相加

数字进行运算时,数据类型不一样不能运算,需要转成一样的才能运算

隐式转换(自动类型提升)

取值范围的数值 → 取值范围的数值

  • 取值范围小的,和取值范围大的进行运算,小的会提升为大的,再进行运算
  • byte short char 三种类型的数据在运算的时候,都会直接先提升为 int,然后再进行运算
强制转换

取值范围的数值 → 取值范围的数值

  • 如果要把一个取值范围大的数值,赋值给取值范围小的变量,是不允许直接赋值的。如果一定要这么做,就需要加入强制转换
  • 格式:目标数据类型 变量名 = (目标数据类型)被强转的数据
1
2
double a = 12.3
int b = (int) a;
1
2
3
byte a = 10;
byte b = 20;
byte result = (byte)(a + b);

字符串相加

  • 当”+”操作中出现字符串时,这个”+”是字符串连接符,而不是算术运算符了。会将前后的数据进行拼接,并产生一个新的字符串
  • 连续进行”+”,从左到右逐个执行
1
System.out.println(10 + 10 + "岁"); // 20岁

字符的 + 操作

  • 当 字符 + 字符/字符 + 数字 时,会把字符通过 ASCII 码表查询到对应的数字再进行计算
1
2
System.out.println(1 + 'a'); // 98
System.out.println('a' + "a"); // aa

自增自减运算符

符号 作用 说明
++ 变量的值加1
变量的值减1

单独使用:++ 和 – 无论是放在变量的前面还是后面,单独写一行结果是一样

1
2
3
4
5
6
7
8
9
int a = 5;
a++;
System.out.println(a); // 6
++a;
System.out.println(a); // 7
a--;
System.out.println(a); // 6
--a;
System.out.println(a); // 5

参与计算:

先用后加

1
2
int a = 10;
int b = a++; // b=10,a=11

先加后用

1
2
int a = 10;
int b = ++a; // b=11,a=11

赋值运算符

符号 作用 说明
= 赋值 int a=10,将10赋值给a
+= 加后赋值 a+=b,将a+b的值给a
-= 减后赋值 a-=b,将a-b的值给a
*= 乘后赋值 a*=b,将a*b的值给a
/= 除后赋值 a/=b,将a÷b的值给a
%= 取余后赋值 a%=b,将a%b的值给a

注意事项:扩展的赋值运算符隐含了强制类型转换

关系运算符

符号 说明
== a==b,判断a和b的值是否相等,成立为true,不成立为false
!= a!=b,判断a和b的值是否不相等,成立为false,不成立为true
> a>b,判断a是否大于b,成立为true,不成立为false
>= a>=b,判断a是否大于等于b,成立为true,不成立为false
< a<b,判断a是否小于b,成立为true,不成立为false
<= a<=b,判断a是否小于等于b,成立为true,不成立为false

注意事项:关系运算符的结果都是boolean类型,要么是true,要么是false

逻辑运算符

符号 作用 说明
& 逻辑与(且) 并且,两边都为真,结果才是真
| 逻辑或 或者,两边都是假,结果才是假
^ 逻辑异或 相同为false,不同为 true
! 逻辑非 取反

短路逻辑运算符

符号 作用 说明
&& 短路与 结果和&相同,但是有短路效果
|| 短路或 结果和|相同,但是有短路效果

注意事项:

  • & | ,无论左边true或false,右边都要执行
  • && || ,当左边的表达式能确定最终的结果,那么右边的表达式则不参与计算了

三元运算符

格式

关系表达式 ? 表达式1 : 表达式2

1
2
int result = a > b ? a : b;	// 把三元运算符的结果赋值给一个变量
System.out.println(a > b ? a : b); // 把三元运算符的结果直接打印

运算符的优先级

优先级 运算符
1 . () {}
2 ! ~ ++ –
3 * / %
4 + -
5 << >> >>>
6 < <= > >= instanceof
7 == !=
8 &
9 ^
10 |
11 &&
12 ||
13 ?:
14 = += -= *= /= %= &=

**原码、反码、补码**

原码:十进制数据的二进制表示,最左边的符号位,0为正,1为负

反码:正数的补码反码是其本身,负数的反码是符号位不变,其余位取反

补码:正数的补码是其本身,负数的补码是在其反码的基础上 +1

原码:利用原码对正数进行计算是不会有问题的。但是如果是负数计算,结果就会出错,实际运算的结果,跟预期是相反的

反码:为了解决原码不能计算负数的问题而出现的

计算规则:正数的反码不变,负数的反码在原码的基础上,符号位不变。数值取反,0变1,1变0

反码的弊端:负数运算的时候,如果结果不跨0,是没有任何问题的,但是如果结果跨0,跟实际结果会有1的偏差

补码:为了解决负数计算时跨0的问题出现的

补码的计算规则:正数的补码不变,负数的补码在反码的基础上+1

另外补码还能多记录一个特殊的值-128,该数据在一个字节下,没有原码和反码

补码的注意点:计算机中的存储和计算都是以补码的形式进行的

十进制数字 原码 反码 补码
+0 0000 0000 0000 0000 0000 0000
-0 1000 0000 1111 1111 0000 0000
-1 1000 0001 1111 1110 1111 1111
-2 1000 0010 1111 1101 1111 1110
-3 1000 0011 1111 1100 1111 1101
-4 1000 0100 1111 1011 1111 1100
-5 1000 0101 1111 1010 1111 1011
-6 1000 0110 1111 1001 1111 1010

?位运算?

运算符 含义 运算规则
& 逻辑与 0为false 1为true
| 逻辑或 0为false 1为true
<< 左移 向左移动,低位补0
>> 右移 向右移动,高位补0或1
>>> 无符号右移 向右移动,高位补0

​ 0000 0000 0000 0000 0000 1100 1000

& 0000 0000 0000 0000 0000 0000 1010

-———————————————————-

​ 0000 0000 0000 0000 0000 0000 1000

​ 0000 0000 0000 0000 0000 1100 1000

| 0000 0000 0000 0000 0000 0000 1010

-———————————————————-

​ 0000 0000 0000 0000 0000 1100 1010

流程控制语句

分支结构

IF语句第一种书写格式

1
2
3
if (关系表达式) {
语句体;
}

执行流程:

  1. 首先计算关系表达式
  2. 如果关系表达式的值为true,就执行语句体
  3. 如果关系表达式为false,就不执行语句体
  4. 继续执行后面的其他语句

if的注意点:

  1. 大括号的开头可以另起一行书写,但是建议写在第一行的末尾
  2. 在语句体中,如果只有一句代码,大括号可以省略不写
  3. 如果对一个布尔类型的变量进行判断,不要用 == 号,直接输入就可以

IF语句第二种书写格式

1
2
3
4
5
if (关系表达式) {
语句体1;
} else {
语句体2;
}

执行流程:

  1. 首先计算关系表达式的值
  2. 如果关系表达式的值为true就执行语句体1
  3. 如果关系表达式的值为false就执行语句体2
  4. 继续执行后面的其他语句

IF语句第三种书写格式

1
2
3
4
5
6
7
if (关系表达式1) {
语句体1;
} else if (关系表达式2) {
语句体2;
} ... else {
语句体n;
}

执行流程:

  1. 首先计算关系表达式1的值
  2. 如果为true就执行语句体1;如果为false就计算关系表达式2的值
  3. 如果true就执行语句体2;如果为false就计算关系表达式3的值
  4. 如果所有关系表达式结果都为false,就执行语句体n

switch语句格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (表达式) {
case1:
语句体1;
break;
case2:
语句体2;
break;
case3:
语句体3
break;
...
default:
语句体n;
break;
}

执行流程:

  1. 首先计算表达式的值
  2. 依次和case后面的值进行比较,如果有对应的值,就会执行相应的语句,在执行的过程中,遇到break就会结束
  3. 如果所有的case后面的值和表达式的值都不匹配,就会执行default里面的语句体,然后结束整个switch语句

格式说明:

  1. 表达式:(将要匹配的值)取值为byte、short、int、char。JDK5以后可以是枚举,JDK7以后可以是String
  2. case:后面跟的是要和表达式进行比较的值(被匹配的值)
  3. break:表示中断,结束的意思,用来结束switch语句
  4. default:表示所有情况都不匹配的时候,就执行该处的内容,和if语句的else相似
  5. case后面的值只能是字面量,不能是变量
  6. case给出的值不允许重复

switch其他知识点

  • default的位置和省略

省略:default可以省略,语法没问题,但不建议省略

位置:default不一定要写在最下面,但是为了方便理解和阅读,习惯写在底部

  • case穿透

如果break漏写,会导致case穿透

执行流程:

  1. 首先还是会拿着小括号中表达式的值跟下面每一个case进行匹配
  2. 如果匹配上了,就会执行相应的语句体,如果发现了break,则会结束整个switch语句
  3. 如果没有发现break,那么程序会继续执行下一个case的语句体,一直遇到break或者右大括号为止

使用场景:

​ 如果多个case的语句体重复了,则可以考虑case穿透去简化代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int dayOfWeek = 2;
String dayType;

switch (dayOfWeek) {
case 1:
case 2:
case 3:
case 4:
case 5:
dayType = "Weekday";
break;
case 6:
case 7:
dayType = "Weekend";
break;
default:
dayType = "Invalid day";
break;
}

System.out.println("The day is: " + dayType);

  • switch新特性

**JDK12以上的版本产生的新特性

1
2
3
4
5
6
7
int num = 1;
switch (num) {
case 1 -> System.out.println("一")
case 2 -> System.out.println("二")
case 3 -> System.out.println("三")
default -> System.out.println("None")
}

用这种语法,可以在case里写表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int number = 10;

String result = switch (number) {
case 1, 2, 3 -> "Number is 1, 2, or 3";
case 4 + 6 -> "Number is 10";
case (int) Math.PI -> "Number is approximately 3";
default -> "Number is not recognized";
};

System.out.println(result);

int a = 5;

String result = switch (a) {
case 1, 2 -> "a is 1 or 2";
case x if x < 10 -> "a is less than 10";
default -> "a is neither 1 nor less than 10";
};

System.out.println(result);

  • switch和if第三种格式各自的使用场景
  1. if的第三种格式一般用于对范围的判断
  2. switch则是把有限个数据一一列举出来,任选其一

循环结构

循环的分类:

  • for
  • while
  • do…while

for循环

格式:

1
2
3
for (初始化语句;条件判断语句;条件控制语句){
循环语句体;
}
1
2
3
for (int i = 1;i <= 10;i++) {
System.out.println("HelloWorld");
}

执行流程:

  1. 执行初始化语句
  2. 条件判断语句,看其结果是true还是false

如果是false,循环结束

如果是true,执行循环体语句

  1. 执行条件控制语句
  2. 回到 2 继续执行条件判断语句

注意点:

  • 初始化语句只执行一次
  • 判断语句为true,循环继续
  • 判断语句为false,循环结束

while循环

格式:

1
2
3
4
while (条件判断语句) {
循环体语句;
条件控制语句;
}
1
2
3
4
5
int i = 1;
while (i <= 100) {
System.out.println(i);
i++;
}

for和while的对比:

相同点:运行规则都是一样的

区别:

  • for 循环中,控制循环的变量,因为归属for循环的语句结构中 在for循环结束后,就不能再次访问到了
  • while 循环中,控制循环的变量,对于while循环来说不归属其语法结构中,在while循环结束后,该变量还可以接续使用

do while循环

格式:

1
2
3
do {
// 循环体代码
} while (条件);
1
2
3
4
5
6
7
8
9
int sum = 0;
int i = 1;

do {
sum += i;
i++;
} while (i <= 10);

System.out.println("Sum: " + sum);

解释:do-while循环是一种后测试循环结构,它首先执行循环体中的代码,然后在条件判断之前检查循环条件。只有在循环体执行完毕后,才会判断条件是否满足,如果满足则继续执行下一次循环,否则退出循环

区别:do-whilewhile的区别就是do-while会先执行循环体,再判断循环体是否符合条件

无限循环

三种格式

for

格式:

1
2
3
for(;;) {
System.out.println("Hello");
}

在后面写代码会报错,因为系统会一直卡在无限循环中,不会执行下面的操作,其他无限循环同理

while

格式:

1
2
3
while(true) {
System.out.println("Hello");
}

do while

格式:

1
2
3
do {
System.out.println("Hello");
} while(true);

跳转控制语句

关键词:continue

格式:

1
2
3
4
5
6
7
8
for (int i = 0; i < 5; i++) {
if (i == 3) {
// 结束本次循环,继续下次循环
continue;
}

System.out.println(i); // 输出结果:0,1,2,4
}

关键词:break

格式:

1
2
3
4
5
6
7
for (int i = 0; i < 5; i++) {
if (i == 3) {
// 结束本次循环,继续下次循环
break;
}
System.out.println(i); // 输出结果:0,1,2
}

数组

数组:数组指一种容器,可以用来存储同种类型的多个值

数组的定义:

格式1:

数据类型 [] 数组名

1
int [] array

格式2“

数据类型 数组名[]

1
int array[]

数组的初始化

初始化:就是在内存中,为数组开辟空间,并将数据存放入容器中的过程

静态初始化

完整格式:

数据类型[] 数组名 = new 数据类型[] {元素1, 元素2, 元素3…} ;

1
int[] array = new int[]{11,22,33};

简化格式:

数据类型[] 数组名 = {元素1, 元素2, 元素3…} ;

1
int[] array = {11,22,33};

地址值:如果直接打印数组名,打印结果会是数组的地址值,即数组在内存中的地址

动态初始化

动态初始化:初始化时只指定数组长度,由系统为数组分配初始值

格式:数据类型[] 数组名 = new 数组类型[数组长度];

1
int[] arr = new int[3];

例:

1
2
3
4
5
// 存50个人的姓名
String[] arrName = new String[50];
// 添加数据
arrName[0] = "张三";
arrName[1] = "李四";

动态初始化、静态初始化的区别

动态初始化:手动指定数组长度,由系统给出默认初始化值

静态初始化:手动指定数组长度。系统会根据元素个数,计算出数组的长度

数组元素访问

格式:数组名[索引];

索引:也叫下标,角标

索引的特点:从0开始,逐个+1增长,持续不间断

例:

1
2
3
4
5
6
7
8
9
// 定义三个身高
double[] height = {160.2,151.5,170.6};
// 定义三个年龄
int[] age = {18,20,19};
// 定义五个姓名
String[] name = {"张三","李四","王五","赵六","孙七"};
System.out.println(height[1]); // 151.1
System.out.println(age[2]); // 19
System.out.println(name[4]); // 孙七

数组的遍历

示例代码:

1
2
3
4
int[] num = {1,2,3,4,5,6,7,8,9,10};
for (int i = 0; i < num.length; i++) {
System.out.println(num[i]);
}

数组的常见问题

索引越界:当访问了数组中不存在的索引值,就会引发索引越界

1
2
int[] num = {1,2,3,4,5,6,7,8};
System.out.println(num[10]); // 索引越界

数组的常见操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 求最值
int[] arr = {95,77,81,90,69,121,55,17,31};
int max = 0;
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]) {max = arr[i];}
}
System.out.println(max);
// 求和
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
System.out.println(sum);
// 交换数据
System.out.println("beforeChange:" + arr[1] + '|' + arr[3]);
int box = 0;
box = arr[1];
arr[1] = arr[3];
arr[3] = box;
System.out.println("afterChange:" + arr[1] + '|' + arr[3]);

二维数组

二维数组的静态初始化

格式:数据类型[]\[] 数组名 = new 数据类型[]\[] {{元素1, 元素2, 元素3},{元素4, 元素5, 元素6},{元素7, 元素8, 元素9}}
1
int[][] arr = new int[][] {{1,2,3},{4,5,6}}
简化格式:数据类型[]\[] 数组名 = {{元素1, 元素2, 元素3},{元素4, 元素5, 元素6},{元素7, 元素8, 元素9}}
1
int[][] arr = {{1,2,3},{4,5,6}}

二维数组的动态初始化

格式:数据类型[][] 数组名 = new 数据类型[行数][列数]

1
int[][] twoDimensionalArray = new int[3][3];

请注意,每行的列数可以不同,因此可以创建不规则的二维数组。例如:

1
2
3
4
int[][] irregularArray = new int[3][];
irregularArray[0] = new int[2];
irregularArray[1] = new int[4];
irregularArray[2] = new int[3];

在上述示例中,我们创建了一个不规则的二维数组irregularArray,其中第一行有2个元素,第二行有4个元素,第三行有3个元素

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 动态初始化一个 3x3 的二维数组
int[][] twoDimensionalArray = new int[3][3];

// 给二维数组赋值
twoDimensionalArray[0][0] = 1;
twoDimensionalArray[0][1] = 2;
twoDimensionalArray[0][2] = 3;
twoDimensionalArray[1][0] = 4;
twoDimensionalArray[1][1] = 5;
twoDimensionalArray[1][2] = 6;
twoDimensionalArray[2][0] = 7;
twoDimensionalArray[2][1] = 8;
twoDimensionalArray[2][2] = 9;

// 打印二维数组的内容
for (int i = 0; i < twoDimensionalArray.length; i++) {
for (int j = 0; j < twoDimensionalArray[i].length; j++) {
System.out.print(twoDimensionalArray[i][j] + " ");
}
System.out.println();
}

二维数组的调用

在Java中,二维数组的索引使用行索引和列索引来访问特定的元素。可以通过在数组名称后使用方括号来指定索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个 3x3 的二维数组
int[][] twoDimensionalArray = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

// 访问二维数组的元素
System.out.println(twoDimensionalArray[0][0]); // 输出: 1
System.out.println(twoDimensionalArray[1][2]); // 输出: 6
System.out.println(twoDimensionalArray[2][1]); // 输出: 8

// 修改二维数组的元素
twoDimensionalArray[1][1] = 10;
System.out.println(twoDimensionalArray[1][1]); // 输出: 10

**Java内存分配**

栈:方法运行时使用的内存,比如main方法运行,进入方法栈中执行

堆:存储对象或者数组,new来创建的,都存储在堆内存

方法区:存储可以运行的class文件

本地方法栈:JVM在使用操作系统功能的时候使用,和开发无关

寄存器:给CPU使用,和开发无关

在Java中,数据类型分为两大类:基本数据类型(Primitive Data Types)和引用数据类型(Reference Data Types)。

基本数据类型是用于存储简单数据值的数据类型,它们是Java语言的内置类型,共有8种基本数据类型:

  1. 整数类型byteshortintlong,用于表示整数值。
  2. 浮点类型floatdouble,用于表示带小数点的数值。
  3. 字符类型char,用于表示单个字符。
  4. 布尔类型boolean,用于表示真(true)或假(false)。

基本数据类型有以下特点:

  • 占用的内存空间固定,不受具体值的影响。
  • 直接存储数据值,而不是对象引用。
  • 在内存中存储在栈(Stack)中。
  • 进行基本数据类型之间的操作时,是按值进行的。

引用数据类型是指非基本数据类型,也称为对象类型(Object Types),包括类(Class)、接口(Interface)、数组(Array)等。引用数据类型通过引用(Reference)来访问和操作对象。

引用数据类型有以下特点:

  • 占用的内存空间不固定,根据对象的大小动态分配。
  • 存储的是对象的引用(内存地址),而不是实际的对象本身。
  • 对象的实际数据存储在堆(Heap)中,通过引用进行访问。
  • 进行引用数据类型之间的操作时,是通过引用进行的,而不是直接操作对象的数据。

通过引用数据类型,我们可以创建和使用自定义的类、接口、数组等复杂数据结构,并调用它们的方法和访问其属性。

方法

方法:方法是程序中最小的执行单元

用处:重复的代码、具有独立功能的代码可以抽取到方法中

好处:提高代码的复用性、可维护性

方法的定义

把一些代码打包在一起,该过程称为方法定义

方法定义一:

不带参数,没有返回值

1
2
3
public static void 方法名() {
方法体(打包起来的代码);
}
1
2
3
4
5
public static void sout() {
System.out.println("你好世界");
System.out.println("Hello World");
System.out.println("Hi World");
}

方法定义二:

带参数,没有返回值

1
2
3
public static void 方法名(形参1,形参2,...) {
方法体(打包起来的代码);
}
1
2
3
4
public static void sum(int num1,int num2) {
int result = num1 + num2;
System.out.println(result);
}

方法定义三:

有返回值

1
2
3
4
public static 返回值类型 calc(int hours) {
方法体(打包起来的代码);
return 返回值;
}
1
2
3
4
public static int calc(int hours) {
int minutes = hours * 60;
return minutes;
}

方法的调用

方法定义后并不是直接运行的,需要手动调用才能执行,该过程称为方法调用

方法调用一:

不带参数

1
2
3
public static void main(String[] args) {
sout();
}

方法调用二:

带参数

1
2
3
public static void main(String[] args) {
sum(50,50);
}

方法调用三:

1
2
3
4
public static void main(String[] args) {
int hours = calc(5);
System.out.println(hours);
}

方法重载

在同一个类中,定义了多个同名的方法,这些同名的方法具有同种功能

每个方法具有不同的参数参数个数,这些同名的方法,就构成了重载关系

即:同一个类中,方法名相同,参数不同的方法。与返回值无关。

参数不同:个数不同、类型不同、顺序不同

例:

1
2
3
4
5
6
public static int sum(int a, int b) {
return a+b;
}
public static int sum(int a, int b,int c) {
return a+b+c;
}

方法内存分配

基本数据类型是用于存储简单数据值的数据类型,它们是Java语言的内置类型,共有8种基本数据类型:

  1. 整数类型byteshortintlong,用于表示整数值。
  2. 浮点类型floatdouble,用于表示带小数点的数值。
  3. 字符类型char,用于表示单个字符。
  4. 布尔类型boolean,用于表示真(true)或假(false)。

基本数据类型有以下特点:

  • 占用的内存空间固定,不受具体值的影响。
  • 直接存储数据值,而不是对象引用。
  • 在内存中存储在栈(Stack)中。
  • 进行基本数据类型之间的操作时,是按值进行的。

引用数据类型是指非基本数据类型,也称为对象类型(Object Types),包括类(Class)、接口(Interface)、数组(Array)等。引用数据类型通过引用(Reference)来访问和操作对象。

引用数据类型有以下特点:

  • 占用的内存空间不固定,根据对象的大小动态分配。
  • 存储的是对象的引用(内存地址),而不是实际的对象本身。
  • 对象的实际数据存储在堆(Heap)中,通过引用进行访问。
  • 进行引用数据类型之间的操作时,是通过引用进行的,而不是直接操作对象的数据。

通过引用数据类型,我们可以创建和使用自定义的类、接口、数组等复杂数据结构,并调用它们的方法和访问其属性。

示例:

1
2
3
4
5
6
7
8
9
10
// 基本数据类型
int num = 10;
double pi = 3.14;
char letter = 'A';
boolean flag = true;

// 引用数据类型
String name = "John";
int[] numbers = {1, 2, 3, 4};
Object obj = new Object();

在这个示例中,numpiletterflag是基本数据类型的变量,它们直接存储相应的值。而name是一个引用数据类型的变量,存储的是字符串对象的引用。numbers是一个引用数据类型的变量,存储的是整型数组对象的引用。obj是一个引用数据类型的变量,存储的是一个通用的对象引用。

总结起来,基本数据类型用于存储简单的数值,而引用数据类型用于存储复杂的对象,并通过引用来访问和操作这些对象。

面向对象

Java的面向对象(Object-Oriented Programming,简称OOP)是一种编程范式,它将程序设计的重点放在对象的创建、操作和交互上,通过模拟现实世界中的对象和它们之间的关系来解决问题。

面向对象的编程思想基于以下概念:

  1. 对象(Object):对象是现实世界中的实体或概念,具有特定的状态(属性)和行为(方法)。
  2. 类(Class):类是对象的模板或蓝图,描述了对象具有的属性和方法。类可以看作是对象的抽象。
  3. 封装(Encapsulation):封装是将数据(属性)和操作(方法)封装在一个单元中,使其成为一个独立的、可复用的组件。通过封装,可以隐藏对象的内部实现细节,只对外提供必要的接口。
  4. 继承(Inheritance):继承是一种机制,允许从现有的类派生出新的类,新类继承了父类的属性和方法,并可以添加自己的特定内容。继承促进了代码的重用和扩展。
  5. 多态(Polymorphism):多态性允许以统一的方式处理不同类型的对象,即同一个方法可以根据调用对象的不同而表现出不同的行为。多态性增加了代码的灵活性和可扩展性。

通过面向对象的方式,可以将复杂的问题分解成更小的、可管理的对象,并通过对象之间的交互来解决问题。面向对象的编程风格使得代码更易于理解、维护和扩展。

Java是一种面向对象的编程语言,它提供了丰富的面向对象的特性和机制,例如类和对象的概念、封装、继承、多态等,使得开发人员可以以面向对象的方式设计和编写程序。

类和对象

对象(Object):对象是现实世界中的实体或概念,具有特定的状态(属性)和行为(方法)。

类(Class):类是对象的模板或蓝图,描述了对象具有的属性和方法。类可以看作是对象的抽象。

如何定义类

格式:

1
2
3
4
public class 类名 {
1. 成员变量
2. 成员方法
}

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Phone {
// 属性(成员变量)
String brand;

// 行为(方法)
public void call() {
...
}

public void playGame() {
...
}
}

如何得到类的对象

格式:

1
类名 对象名 = new 类名();

示例:

1
Phone p = new Phone();

如何使用对象

访问属性:

1
对象名.成员变量

访问行为:

1
对象名.方法名()

定义类的注意事项

用来描述一类事务的类,专业叫做:Javabean类

在Javabean类中,是不写main方法的

编写main的类,叫做测试类

可以在测试类中创建Javabean类的对象并进行赋值调用

类名首字母建议大写(驼峰命名法)

一个Java文件中可以定义多个class类,且只能一个类是public修饰,而且public修饰的类名必须称为代码文件名

实际开发中建议还是一个文件定义一个class类

成员变量的完整定义格式是:

1
修饰符 数据类型 变量名称 = 初始化值;

一般无需指定初始化值,存在默认值;

数据类型 明细 默认值
基本类型 byte、short、int、long 0
基本类型 float、double 0.0
基本类型 boolean false
引用类型 类、接口、数组、String null

封装

封装:如何正确设计对象的属性和方法

对象代表什么,就得封装对应的数据,并提供数据对应的行为

Private关键字

  • 是一个权限修饰符
  • 可以修饰成员(成员变量和成员方法)
  • private修饰的成员只能在本类中才能访问

例:

1
2
3
4
5
public class Phone {
private String picture;
String brand;
double price;
}
  • 针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作
  • 提供”setXXX(参数)”方法,用于给成员变量赋值,方法用public修饰
  • 提供”getXXX()”方法,用于获取成员变量的值,方法用public修饰

this关键字

成员变量和局部变量

1
2
3
4
5
6
7
public class Person {
private int age; // 成员变量(全局变量)
public void age() {
int age = 18; // 局部变量
System.out.println(age); // 这里的age会优先获取局部变量的age(就近原则)
}
}

如果想要获取到外部的变量,则需要用到this关键字

1
2
3
4
5
6
7
public class Person {
private int age; // 成员变量(全局变量)
public void age() {
int age = 18; // 局部变量
System.out.println(this.age); // 这里的age会获取全部变量的age
}
}

在Java中,this 是一个关键字,它是一个引用,指向当前对象的实例。

当在类的实例方法中使用 this 关键字时,它表示对当前正在调用方法的对象的引用。它可以用于访问当前对象的成员变量、调用当前对象的其他方法,或者将当前对象作为参数传递给其他方法。

以下是一些使用 this 关键字的常见情况:

  1. 访问实例变量:使用 this 关键字可以在方法中访问当前对象的实例变量。例如:

    1
    2
    3
    4
    5
    6
    7
    public class Person {
    private String name;

    public void setName(String name) {
    this.name = name;
    }
    }

    在上述示例中,通过 this.name 可以访问当前对象的 name 实例变量。

  2. 调用其他方法:使用 this 关键字可以在一个方法中调用同一对象的其他方法。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Calculator {
    private int result;

    public void add(int num) {
    this.result += num;
    }

    public void subtract(int num) {
    this.result -= num;
    }
    }

    在上述示例中,this.result 用于在 addsubtract 方法中引用当前对象的 result 成员变量。

  3. 在构造方法中调用其他构造方法:使用 this 关键字可以在一个构造方法中调用同一类的其他构造方法。这种方式称为构造方法的重载。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Rectangle {
    private int width;
    private int height;

    public Rectangle() {
    this(0, 0); // 调用带参数的构造方法
    }

    public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
    }
    }

    在上述示例中,无参的构造方法通过 this(0, 0) 调用带参数的构造方法。

需要注意的是,this 关键字只能在非静态方法(实例方法)中使用,因为它指向当前对象的实例。在静态方法中无法使用 this 关键字,因为静态方法是与类关联而不是与对象关联的。

总而言之,this 关键字用于在对象的实例方法中引用当前对象,以便访问实例变量和调用其他方法。

this关键字的本质:代表方法调用者的地址值

构造方法(构造函数)

作用:在创建对象的时候给成员变量进行赋值的

格式:

1
2
3
4
5
public class Peson {
修饰符 类名(参数) {
方法体;
}
}

特点:

  1. 方法名和类名相同,大小写敏感
  2. 没有返回值类型,也没有void
  3. 没有具体的返回值(不能由return带回结果数据)

例:

1
2
3
4
5
6
7
8
9
10
11
12
public class Student {
private String name;
private int age;

public Student() { // 空参构造方法
...
}

public Student(String name,int age) { // 带全部参数构造方法
...
}
}

执行时机:

  1. 创建对象的时候由虚拟机调用,不能手动调用构造方法
  2. 每创建一次对象,就会调用一次构造函数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Student {
private String name;
private int age;

public Student() { // 空参构造方法

}

public Student(String name,int age) { // 带全部参数构造方法
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}
1
2
3
4
5
6
7
8
9
// 传递以及获取类的值
public class StudentTest {
public static void main(String[] args) {
Student s = new Student("张三",18)

System.out.println(s.getName());
System.out.println(s.getAge());
}
}

注意事项:

  • 如果没有写任何的构造方法,虚拟机会自己创建一个空参构造方法
  • 如果定义了构造方法,系统将不再提供默认的构造方法

详细说明:

构造方法(Constructor)是一种特殊的方法,在Java类中用于创建和初始化对象。构造方法的主要作用是在创建对象时对其进行初始化,确保对象在使用之前处于合理的状态。它有以下几个重要的作用:

  1. 对象的创建:构造方法用于创建对象。在使用new关键字实例化一个类时,会调用该类的构造方法,从而创建一个对象。
  2. 对象的初始化:构造方法在创建对象时对其进行初始化,可以在构造方法中设置对象的初始状态,给成员变量赋初值,执行一些必要的初始化操作。
  3. 确保对象的合理状态:通过在构造方法中对对象进行初始化,可以确保对象在被使用之前处于合理的状态,避免了未初始化或不完整对象的使用。
  4. 可以重载:与普通方法一样,构造方法也可以进行重载。即在同一个类中可以定义多个构造方法,通过不同的参数列表来实现不同的初始化方式。

构造方法的命名与类名相同,没有返回类型(甚至没有void关键字),且无需显式调用,它会在创建对象时自动调用。如果在类中没有显式定义构造方法,Java会提供一个默认的无参构造方法,用于创建对象,但如果在类中定义了带参构造方法,则默认的无参构造方法将不再提供,需要显式定义。

以下是一个简单的示例,展示了构造方法的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Person {
String name;
int age;

// 无参构造方法,默认提供,可以省略显式定义
public Person() {
// 对象初始化
name = "Unknown";
age = 0;
}

// 带参构造方法,重载构造方法
public Person(String name, int age) {
// 对象初始化
this.name = name;
this.age = age;
}
}

public class Main {
public static void main(String[] args) {
// 使用无参构造方法创建对象
Person person1 = new Person();
System.out.println(person1.name); // Output: "Unknown"
System.out.println(person1.age); // Output: 0

// 使用带参构造方法创建对象
Person person2 = new Person("Alice", 30);
System.out.println(person2.name); // Output: "Alice"
System.out.println(person2.age); // Output: 30
}
}

标准的JavaBean类

  • 类名需要见名知意

  • 成员变量使用private修饰

  • 提供至少两个构造方法

    1. 无参构造方法
    2. 带全部参数的构造方法
  • 成员方法

    1. 提供每一个成员变量的set和get
    2. 如果还有其他行为,也需要写上

成员变量和局部变量

成员变量和局部变量的区别

区别 成员变量 局部变量
类中位置不同 类中,方法外 方法内、方法申明上
初始化值不同 有默认初始值 没有默认初始值,使用前需要完成赋值
内存位置不同 堆内存的对象 栈内存
生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用存在,调用结束后消失
作用域 整个类中有效 当前方法中有效

API&String

API

API:应用程序编程接口

JavaAPI:JDK中提供的各种功能的Java类

这些类将底层的实现封装了起来,需要时直接调用即可

JDK API即官方API文档,可以查询API的具体用法、用处

String

  1. String是Java定义好的一个类。定义在java.lang包中,所以使用时不需要导包
  2. Java程序中的所有字符串文字都被实为此类的对象
  3. 字符产不可改变,它们的值在创建后不能被更改

创建String对象的方式

  1. 直接赋值
1
String name = "张三"
  1. new
构造方法 说明
public String() 创建空白字符串,不包含任何内容
public String(String original) 根据传入的字符串,创建字符串对象
public String(char[] chs) 根据字符数组,创建字符串对象
public String(byte[] chs) 根据字节数组,创建字符串对象
1
2
3
4
5
6
7
8
9
10
11
12
13
String s1 = new String();
System.out.println(s1); // "" 即空字符串

String s2 = new String("abc");
System.out.println(s2); // "abc"

char[] chs = {'h','e','l','l','o'};
String s3 = new String(chs);
System.out.println(s3); // "hello"

byte[] bytes = {97,98,99,100};
String s4 = new String(bytes);
System.out.println(s4) // "abcd" ASCII码

当使用直接赋值的方法赋值时,系统会检查该字符串是否存在

不存在则创建新的,若存在则复用

1
2
String s1 = "abc";
String s2 = "abc"; // s1 s2执行的是同一个内存地址

字符串比较

由于字符串属于引用数据类型,使用”==”比较时,实则是比较的地址值,用new创建两个字符串,即使值是一样的,但是地址不同,则比较结果是 false

1
2
基本数据类型:比较数据值
引用数据类型:比较地址值

Java提供了两个方法专门用于字符串比较

  • boolean equals方法(要比较的字符串) 完全一样结果才是true,否则为false
  • boolean equalslgnoreCase(要比较的字符串) 忽略大小写的比较
1
2
3
4
5
String s1 = new String("abc");
String s2 = "abc";

boolean res = s1.equals(s2);
System.out.println(res); // true

遍历字符串

关键字:charAt

public char charAt(int index):根据索引返回字符串

public int length():返回此字符串的长度

1
2
3
4
5
String s1 = "abc";
for (int i=0;i < s1.length;i++) {
char c = str.charAt(i)
System.out.println(c); // 依次打印:a b c
}

StringBuilder

StringBuilder可以看作是一个容器,创建之后里面的内容是可变的

作用:提高字符串的效率

方法名 说明
public StringBuilder() 创建一个空白可变字符串对象,不含有任何内容
public StringBuilder(String str) 根据字符串的内容,来创建可变字符串对象
1
2
StringBuilder sb1 = new StringBuilder("abc");	// 有参
StringBuilder sb2 = new StringBuilder(); // 无参

stringBuilder常用方法

方法名 说明
public StringBuilder append(任意类型) 添加数据,并返回对象本身
public StringBuilder reverse() 反转容器中的内容
public int length() 返回长度(字符出现的个数)
public String toString() 通过toString,还原字符串类型

具体用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 创建对象
StringBuilder sb = new StringBuilder();
sb.append(123);
sb.append(2.3);
sb.append(true);
// 打印
// Java对这个类进行了特殊处理
// 打印对象不是地址值而是属性值
System.out.println(sb); // 1232.3true

// 反转
sb.reverse();
// 获取对象字符串长度
int len = sb.length();
System.out.println(sb); // eurt3.2321
System.out.println(len); // 10

// 转换为字符串类型
String str = sb.toString();
System.out.println(str); // eurt3.2321

链式编程:当调用一个方法的时候,不需要用变量接受他的结果,可以继续调用其他方法

例如:

1
2
sb.append(123).reverse();
System.out.println("321");

StringJoiner

StringJoiner用于拼接字符串,和StringBuilder一样,也可以看作是一个容器,里面的内容是可以改变的

方法名 说明
public StringJoiner(间隔符号) 创建一个StringJoiner对象,指定拼接时的间隔符号
public StringJoiner(间隔符号,开始符号,结束符号) 创建一个StringJoiner对象,指定拼接时的间隔符号、开始符号、结束符号

成员方法

方法名 说明
public StringJoiner add(添加的内容) 添加数据,并返回对象本身
public int length() 返回长度(字符出现的个数)
public String toString() 返回一个字符串(还原字符串)

ArrayList(集合)

集合和数组的区别

数组:

  • 长度固定
  • 可以存储基本数据类型
  • 可以存储引用数据类型

集合:

  • 长度不固定
  • 只能存储引用数据类型
  • 如果要存储基本数据类型,需要转换成包装类

ArrayList基本使用

示例:

1
2
3
4
5
6
// 1.创建集合的对象
// 泛型(e):限定集合中存储数据的类型
ArrayList<String> list = new ArrayList<>();
// 这个类在底层做了一些处理,打印对象不是地址值,而是具体的数据
// 在展示的时候会拿[]把所有的数据进行包裹
System.out.println(list); // 打印结果:[]

ArrayList成员方法

方法名 说明
boolean add(E e) 添加元素,返回值表示是否添加成功
boolean remove(E e) 删除指定元素,返回值表示是否删除成功
E remove(int index) 删除指定索引的元素,返回被删除元素
E set(int index,E e) 修改指定索引下的元素,返回原来的元素
E get(int index) 获取指定索引的元素
int size() 集合的长度,也就是集合中元素的个数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 添加元素
list.add("你好");
list.add("Hello");
System.out.println(list); // 打印结果:[你好, Hello]

// 删除元素
list.remove("你好");
System.out.println(list); // 打印结果:[Hello]
// 以上两个都有返回值,且为布尔值
boolean res1 = list.add("test");
boolean res2 = list.remove("test");
boolean res3 = list.remove("???"); // 删除集合中不存在的元素返回值为false
System.out.println(res1 + "-" + res2 + "-" + res3); // 打印结果:true-tr

// 按照索引删除元素
String delete = list.remove(0);
System.out.println(delete); // 打印结果:Hello
System.out.println(list); // 打印结果:[]

// 修改元素
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
String change = list.set(0,"first");
System.out.println(change); // aaa
System.out.println(list); // [first, bbb, ccc, ddd]

// 查询元素
String find = list.get(3);
System.out.println(find); // ddd

// 获取集合的长度
int size = list.size();
System.out.println(size); // 4

包装类

由于集合不能直接存储基本数据类型,所以需要将基本数据类型转换为包装类

基本数据类型 包装类
byte Byte
short Short
char Character
int Integer
long Long
float Float
double Double
boolean Boolean

规律就是将原来的数据类型改为大写开头,只需要记住char-Character,int-Integer

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 创建集合
ArrayList<Integer> intArr = new ArrayList<>();
ArrayList<Character> charArr = new ArrayList<>();


intArr.add(1);
intArr.add(2);
intArr.add(3);
intArr.add(4);
intArr.add(5);
// 遍历数字集合
for (int i = 0; i < intArr.size(); i++) {
System.out.println(intArr.get(i));
}


charArr.add('a');
charArr.add('b');
charArr.add('c');
charArr.add('d');
charArr.add('e');
charArr.add('f');
// 遍历字符集合
for (int i = 0; i < charArr.size(); i++) {
System.out.println(charArr.get(i));
}

面向对象进阶

static

static表示静态的意思,是java中的一个修饰符,可以修饰成员方法,成员变量

1
2
3
4
5
6
7
 // 属性:学生姓名 年龄 性别 老师姓名
private String name;
private int age;
private String gender;

// 所有的对象都共享一个老师姓名
public static String teacherName;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 所有的对象都共享一个老师姓名
Student.teacherName = "Bob";

Student s1 = new Student();
s1.setName("张三");
s1.setGender("男");
s1.setAge(23);
s1.study();
s1.show();
Student s2 = new Student();
s2.setName("李四");
s2.setGender("女");
s2.setAge(22);
s2.study();
s2.show();

static修饰的成员变量,叫做静态变量

特点:

  • 被该类所有对象共享
  • 不属于对象,属于类
  • 随着类的加载而加载,优先于对象存在

调用方式:

  • 类名调用(推荐)
  • 对象名调用

static修饰的成员方法,叫做静态方法

特点:

  • 多用在测试类和工具类中
  • Javabean类中很少会用

调用方式:

  • 类名调用(推荐)
  • 对象名调用

目前学习的共有三种类:

  1. Javabean类
    用来描述一类事物的类。比如:Student、Teacher、Dog、Cat
  2. 测试类
    用来检查其他类是否书写正确,带有main方法的类,是程序的入口
  3. 工具类
    不是用来描述一类事物的,而是帮我们做一些事情的类。比如Java里的random、math

static注意事项

  • 静态方法只能访问静态变量和静态方法
  • 非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法
  • 静态方法中是没有this关键字

总结:

  1. 静态方法中,只能访问静态
  2. 非静态方法可以访问所有
  3. 静态方法中没有this关键字

在 Java 中,静态方法(Static Method)是属于类而不是对象的,因此在静态方法中没有 this 关键字。

静态方法是与类相关联的,它不依赖于类的实例(对象)而存在,可以直接通过类名来调用。由于静态方法不属于特定的对象,因此在静态方法中无法使用 this 关键字来引用对象本身,因为静态方法不知道具体是哪个对象在调用它。

相反,在实例方法(非静态方法)中,可以使用 this 关键字来引用调用该方法的对象本身,因为实例方法是属于对象的。

以下是一个示例,展示了静态方法和实例方法的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyClass {
private int value;

public static void staticMethod() {
// 静态方法无法使用 this 关键字
// this.value = 10; // 编译错误:Cannot use this in a static context
}

public void instanceMethod() {
// 实例方法可以使用 this 关键字
this.value = 10;
}
}

在上述示例中,staticMethod 是一个静态方法,它试图使用 this 关键字来引用 value 属性,但会导致编译错误。而 instanceMethod 是一个实例方法,它可以正常使用 this 关键字来引用 value 属性。

总结一下,静态方法是属于类的方法,不依赖于对象;而实例方法是属于对象的方法,可以使用 this 关键字来引用对象本身。

继承

Java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系

1
public class Student extends Person {}

Student称为子类(派生类),Person称为父类(基类或超类)

继承的概述

Java中的继承是面向对象编程的一种重要特性,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以获得父类的所有非私有成员变量和方法,并且可以在此基础上添加自己的新成员变量和方法。

继承的特点和优势包括:

  1. 代码重用: 继承允许子类重用父类的代码,避免了重复编写相同的代码,提高了代码的可维护性和复用性。
  2. 层次结构: 继承可以形成类之间的层次结构,允许按照共性和特性进行分类和组织,从而更好地组织和管理代码。
  3. 多态性: 继承是实现多态性的基础。通过父类类型的引用指向子类对象,可以在运行时动态地调用子类的方法,实现了多态性。

在Java中,使用 extends 关键字来声明继承关系。子类使用 extends 关键字后面跟着父类的名称来声明继承,子类就会继承父类的成员变量和方法。

以下是一个简单的Java继承示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 父类
class Animal {
void eat() {
System.out.println("Animal is eating.");
}
}

// 子类继承自Animal类
class Dog extends Animal {
void bark() {
System.out.println("Dog is barking.");
}
}

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 继承自Animal类的方法
dog.bark(); // 子类独有的方法
}
}

在上述示例中,Animal 是父类,Dog 是子类。Dog 类继承了 Animal 类的 eat() 方法,并且在此基础上添加了 bark() 方法。在 main 方法中,我们可以创建 Dog 类的对象并调用继承自父类的方法和子类自己的方法。

需要注意的是,Java中的继承是单继承的,一个类只能继承自一个父类。但是,一个类可以实现多个接口(接口之间是多继承的关系)。

Java只支持单继承,不支持多继承,但支持多层继承

单继承:一个子类只能继承一个父类

不支持多继承:子类不能同时继承多个父类

多层继承:子类A继承父类B,父类B可以继承父类C

每一个类都直接或间接的继承与Object(即虚拟机会将程序员编写的类自动继承Java中的Object)

继承的使用

当类与类之间,存在相同(共性)的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码

当构思继承的时候,不推荐直接大脑思考,而是使用思维导图的方式,把子类的共性内容抽取到父类中

子类与父类的继承

子类能继承父类中的哪些内容

构造方法 非私有(不能继承) 私有/private(不能继承)
成员变量 非私有(能继承) 私有/private(能继承)
成员方法 非私有(能继承) 私有/private(不能继承)

构造方法

在 Java 中,构造方法(Constructor)不是普通的方法,它有特殊的用途和规则。构造方法是用于创建对象的特殊方法,它在创建对象时被调用,并负责初始化对象的状态。

由于构造方法的特殊性,它并不能被继承。在子类中不会自动继承父类的构造方法,而是由子类自己定义构造方法。当子类创建对象时,必须调用自己的构造方法来初始化自己的实例变量。

当子类没有定义构造方法时,编译器会默认提供一个无参构造方法(如果父类有无参构造方法),或者继承父类的构造方法(如果父类有其他构造方法但没有无参构造方法)。

如果子类定义了构造方法,那么它不会继承父类的构造方法。在子类的构造方法中,可以通过使用 super 关键字调用父类的构造方法来完成对父类的初始化。

以下是一个简单示例来说明构造方法不会被继承的情况:

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

Parent(int age) {
this.age = age;
}
}

class Child extends Parent {
String name;

Child(String name, int age) {
super(age); // 调用父类的构造方法进行初始化
this.name = name;
}
}

在上述示例中,Parent 类有一个带参数的构造方法,Child 类继承自 Parent 类。在 Child 类的构造方法中,我们使用 super(age) 调用了父类的构造方法来初始化父类的 age 属性,然后再初始化子类自己的 name 属性。

总结一下,构造方法不会被继承,子类必须定义自己的构造方法来完成对自己和父类的初始化。但是,可以通过 super 关键字调用父类的构造方法来辅助完成初始化过程。

成员变量

在Java中,继承(Inheritance)允许子类继承父类的非私有成员变量,包括公共(public)、受保护(protected)和默认(包级私有,默认修饰符,没有关键字修饰)访问修饰符修饰的成员变量。但是,子类无法直接访问继承的私有(private)成员变量。

继承允许子类获取父类的属性和方法,并在子类中进行重用。这样做的好处是可以提高代码的复用性和可维护性。继承的非私有成员变量在子类中可以使用,并且通过子类的实例可以访问这些继承的成员变量。

然而,父类的私有成员变量是被封装的,它们只能在父类的内部使用,无法被子类直接访问。子类无法继承父类的私有成员变量,因此无法通过子类的实例来访问这些私有成员变量。

以下是一个简单的示例来说明继承中对成员变量的访问权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Parent {
public int publicVar;
protected int protectedVar;
int defaultVar; // 默认访问修饰符,包级私有
private int privateVar;

public Parent() {
publicVar = 1;
protectedVar = 2;
defaultVar = 3;
privateVar = 4;
}
}

class Child extends Parent {
void printVariables() {
System.out.println(publicVar); // 可以访问继承的公共成员变量
System.out.println(protectedVar); // 可以访问继承的受保护成员变量
System.out.println(defaultVar); // 可以访问继承的默认成员变量
// System.out.println(privateVar); // 编译错误,无法访问继承的私有成员变量
}
}

public class Main {
public static void main(String[] args) {
Child child = new Child();
child.printVariables();
}
}

在上述示例中,Parent 类有四个成员变量,分别使用了不同的访问修饰符。Child 类继承了 Parent 类,可以访问 Parent 类中的公共、受保护和默认成员变量,但无法直接访问 Parent 类的私有成员变量。

虚方法表

虚方法表(Virtual Method Table,简称Vtable)是Java虚拟机(JVM)在运行时用于支持多态性的一种数据结构。它是面向对象编程语言中实现动态绑定(Dynamic Binding)的一种机制。

在Java中,动态绑定是指在运行时根据对象的实际类型来确定调用哪个方法,而不是在编译时根据引用变量的声明类型来确定。这使得子类可以重写父类的方法,然后在运行时根据实际的对象类型调用相应的子类方法,实现了多态性。

虚方法表是实现动态绑定的重要工具。每个类在Java虚拟机中都有一个虚方法表其中存储了该类的虚方法信息,包括方法的地址(指向实际的方法代码)。虚方法表是针对类而不是对象的,因此每个类只有一个虚方法表

当调用对象的虚方法时,虚方法表会根据对象的实际类型查找相应的方法地址,然后进行方法调用。这样,就能实现在运行时动态地选择正确的方法,而不依赖于引用变量的类型。

虚方法表的使用使得Java中的方法调用变得更加灵活和多态,使得继承和方法重写得以实现。这也是Java等面向对象语言能够实现多态性的基础。

只有父类中的虚方法才能被子类继承

虚方法:非private、非static、非final

继承范围

成员变量、成员方法

就近原则:现在局部位置找,本类成员位置找,父类成员位置找,逐级往上

super调用,直接访问父类

1
2
3
4
5
6
7
8
9
10
11
public class Fu {
String name = "Fu";
}

public class Zi extends Fu{
String name = "Zi"; // 删除这行,打印结果为:Fu
public void ziShow(){
String name = "ziShow"; // 删除这行,打印结果为:Zi
System.out.println(name); // 结果:ziShow
}
}

以下是综合案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Fu {
int Id = 10;

public void doThings() {
System.out.println("I am Fu");
}
}


public class Zi extends Fu{
int Id = 20;
public void idShow() {
System.out.println("Zi:" + this.Id);
System.out.println("Fu:" + super.Id);
}

public void doThings() {
System.out.println("I am Zi");
}

public void ziShow() {
this.doThings();
}

public void fuShow() {
super.doThings();
}
}

public class Test {
public static void main(String[] args) {
Zi z1 = new Zi();
z1.idShow(); // Zi: 20 Fu:10
z1.ziShow();
z1.fuShow();
}
}

方法的重写

当父类的方法不能满足子类现在的需求时,需要进行方法重写

书写格式

在继承体系中,子类出现了和父类中一摸一样的方法声明,我们就称这个方法是重写的方法

@Override重写注解

  1. @Override是放在重写后的方法上,校验子类重写时语法是否正确
  2. 加上注解后如果有红色波浪线,表示语法错误
  3. 建议重写方法都加@Override注解,代码安全,优雅!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person {
public void eat() {
System.out.println("吃米饭");
}
public void drink() {
System.out.println("喝开水");
}
}

public class Student extends Person{
@Override
public void eat() {
System.out.println("吃菜");
}
@Override
public void drink() {
System.out.println("喝饮料");
}
}

注意事项:

  1. 重写方法的名称、形参列表必须与父类中的一致
  2. 子类重写父类方法时,访问权限子类必须大于等于父类(暂时了解:空着不写 < protected < public)
  3. 子类重写父类方法时,返回值类型子类必须小于等于父类
  4. 总结(建议):重写的方法尽量和父类保持一致
  5. 只有被添加到虚方法表中的方法才能被重写

继承中:构造方法的访问特点

  • 父类中的构造方法不会被子类继承
  • 子类中所有的构造方法默认先访问父类中的无参构造,再执行自己

↑解释:

  • 子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
  • 子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化

怎么调用父类构造方法?

  • 子类构造方法的第一行语句默认都是:super(),不写也存在,且必须在第一行
  • 如果想调用父类的有参构造,必须手动写super进行调用

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Fu{
int Age;
String Name;
public Fu() {
Age = 10;
Name = "张三";
}
public Fu(int Age,String Name) {
this.Age = Age;
this.Name = Name;
}
}

public class Zi extends Fu{
public Zi() {
// 子类构造方法中隐藏的super()去访问父类的无参构造
super();
}

public Zi(String Name,int Age) {
// 访问父类的有参构造
super(Name,Age);
}
}

多态

多态:同类型的对象,表现出的不同形态

多态的表现形式

1
父类类型 对象名称 = 子类对象;

多态前提

  • 有继承关系
  • 有父类引用指向子类对象
  • 有方法重写
1
Fu f = new Zi();

例子:

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

public class Test {
public static void main(String[] args) {
Animal a = new Dog();
// 变量为父,方法为子
System.out.println(a.name); // 动物
a.show(); // Dog----show
}
}

class Animal {
String name = "动物";

public void show() {
System.out.println("Animal----show");
}
}

class Dog extends Animal {
String name = "狗";

@Override
public void show() {
System.out.println("Dog----show");
}
}

解释:

成员变量:在子类的对象中,会把父类的成员变量也继承下来。

成员方法:如果子类对方法进行了重写,那么在虚方法表中是会把父类的方法进行覆盖的。

编译看左边,运行看右边

多态的优势和弊端

优势

  • 在多态的形式下,右边对象可以实现解耦合,便于拓展和维护
1
2
3
Person p = new Student();
p.work();
// 如果需要变成teacher的work方法,不需要修改其他地方,只需要把new Student() 改为 new Teacher()
  • 定义方法的时候,使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利

弊端

  • 不能调用子类特有方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package duotai;

public class Test {
public static void main(String[] args) {
Animal a = new Dog();
// 变量为父,方法为子
System.out.println(a.name); // 动物
a.show(); // Dog----show
// a.watchDoor(); 父类里面没有这个方法,会报错
}
}

class Animal {
String name = "动物";

public void show() {
System.out.println("Animal----show");
}
}

class Dog extends Animal {
String name = "狗";

@Override
public void show() {
System.out.println("Dog----show");
}

public void watchDoor() {
System.out.printf("看门");
}
}

包、final、权限修饰符、代码块

包就是文件夹。用来管理各种不同功能的Java类,方便后期代码维护

  • 包名的规则:公司域名反写 + 包的作用,需要全部英文小写,见名知意,如:com.zhaojiale.domain.student
1
2
3
4
5
public class Test {
public static void main(String[] args) {
com.zhaojiale.domain.Student s = new com.zhaojiale.domain.Student()
}
}

||

1
2
3
4
5
6
import com.zhaojiale.domain.Student;
public class Test {
public static void main(String[] args) {
Student s = new Student()
}
}

使用其他类的规则

  • 使用同一个包中的类时,不需要导包
  • 使用Java.lang包中的类时,不需要导包
  • 其他情况都需要导包
  • 如果同时使用两个包中的同名类,需要用全类名

Final

修饰方法:表明该方法是最终方法,不能被重写

修饰类:表明该类是最终类,不能被继承

修饰变量:叫做常量,只能被赋值一次

权限修饰符

权限修饰符的分类

有四种作用范围由大到小(private < 缺省 < protected < public)

修饰符 同一类中 同一包中其他类 不同包下的子类 不同包下的无关类
private
缺省
protected
public

实际开发中,一般只用private和public

  • 成员变量私有
  • 方法公开

静态代码块

格式:

1
static{}

特点:

需要通过static关键字修饰,随着类的加载而加载,并且自动触发,只执行一次

使用场景:

在类加载的时候,做一些数据初始化的时候使用

执行时机:

随着类的加载而加载

1
2
3
4
5
6
7
8
9
10
public class Student{
private String name;
private int age;

static {
System.out.println("静态代码块执行了");
}

...
}

抽象类/抽象方法

具体实例在资料的 day12、day13中

抽象方法:

将共性的行为(方法)抽取到父类之后。由于每一个子类执行的内容是不一样,所以在父类中不能确定具体的方法体。该方法就可以定义为抽象方法

抽象类:

如果一个类中存在抽象方法,那么该类就必须声明为抽象类

  • 抽象方法的定义格式
1
public abstract 返回值类型 方法名(参数列表);
  • 抽象类的定义格式
1
public abstract class 类名{}

抽象类和抽象方法的注意事项

  • 抽象类不能实例化(实例化:创建对象)
  • 抽象类中不一定有抽象方法。有抽象方法,类一定是抽象类
  • 可以有构造方法
  • 抽象类的子类
    • 要么重写抽象类中的所有抽象方法
    • 要么是抽象类

1
2
3
4
5
6
7
8
9
10
11
package abstractDemo;

abstract class Person {
public abstract void work();
}

public class Test {
public static void main(String[] args) {
// Person p = new Person(); java: abstractDemo.Person是抽象的; 无法实例化
}
}

接口

接口就是一种规则,是对行为的抽象

定义接口

  • 接口用关键字interface来定义
1
public interface 接口名 {}
  • 接口不能实例化
  • 接口和类之间是实现关系,通过implements关键字表示
1
public class 类名 implements 接口名 {}
  • 接口的子类(实现类)
    • 要么重写接口中的所有抽象方法
    • 要么是抽象类

注意:

  1. 接口和类的实现关系,可以单实现,也可以多实现。
1
public class 类名 implements 接口名1,接口名2 {}
  1. 实现类还可以在继承一个类的同时实现多个接口
1
public class 类名 extends 父类 implements 接口名1,接口名2 {}

接口中成员的特点

  • 成员变量
    • 只能是常量
    • 默认修饰符:public static final
  • 构造方法
    • 没有
  • 成员方法
    • 只能是抽象方法
    • 默认修饰符:public abstract
  • JDK7以前:接口中只能定义抽象方法
  • JDK8的新特性:接口中可以定义有方法体的方法
  • JDK9的新特性:接口中可以定义私有方法

接口和类之间的关系

  • 类和类的关系:

    继承关系,只能单继承,不能多继承,但是可以多层继承

  • 类和接口的关系

    实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口

  • 接口和接口的关系

    继承关系,可以单继承,也可以多继承

拓展

JDK8以后接口中新增的方法

  • 允许在接口中定义默认方法,需要使用关键字 default 修饰
    • 作用:解决接口升级问题

接口中默认方法的定义格式

  • 格式:public default 返回值类型 方法名(参数列表) {}
  • 范例:public default void show() {}

接口中默认方法的注意事项

  • 默认方法不是抽象方法,所以不强制被重写。但如果被重写,重写的时候去掉 default 关键字
  • public可以省略,default不能省略
  • 如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写

内部类

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

内部类:在一个类的里面,再定义一个类

1
2
3
4
5
public class Outer {	// 外部类
public class Inner { // 内部类

}
}

内部类表示的事物是外部类的一部分

内部类单独出现没有任何意义

内部类的访问特点:

  • 内部类可以直接访问外部类的成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象

静态内部类

1
2
3
4
5
public class Outer {	// 外部类
static class Inner { // 内部类

}
}

静态内部类只能访问外部类中的静态变量和方法,如果想要访问非静态的需要创建对象

创建静态内部类对象的格式:

1
外部类名.内部类名 对象名 = new 外部类名.内部类名();

调用非静态方法的格式:先创建对象,用对象调用

调用静态方法的格式:外部类名.内部类名.方法名()

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

public class Outer {
int a = 10;
static int b = 20;
static class Inner {
public void show1() {
Outer o = new Outer();
System.out.println(o.a);
System.out.println(b);
}

public static void show2() {
Outer o = new Outer();
System.out.println(o.a);
System.out.println(b);
}
}
}
1
2
3
4
5
6
7
8
9
10
package InnerClass;

public class Test {
public static void main(String[] args) {
Outer.Inner oi = new Outer.Inner();
oi.show1();
// oi.show2(); 不会报错且可以使用
Outer.Inner.show2();
}
}

局部内部类

  1. 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量
  2. 外界是无法直接使用,需要在方法内部创建对象并使用
  3. 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package InnerClass02;

public class Outer {
int b = 20;
public void show() {
int a = 10;
class Inner{
String name;
int age;

public void method1() {
System.out.println("局部内部类中的method1方法");
}

public static void method2() {
System.out.printf("局部内部类中的method2静态方法");
}
}

// 创建局部内部类的对象
Inner i = new Inner();
System.out.println(i.name);
System.out.println(i.age);
i.method1();
Inner.method2();
}
}

匿名内部类

匿名内部类本质上就是隐藏了名字的内部类

格式:

1
2
3
new 类名或接口名() {
重写方法;
}

举例:

1
2
3
4
5
new Inter() {
public void show() {

}
}

实际用途:

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

public class Test2 {
public static void main(String[] args) {
// 在测试类中调用下面的method方法?
// 以前的方式如何调用?
// 要自己写一个子类继承Animal类
// 再创建子类的对象
// Dog d = new Dog();
// method(d);
// 以上的方法过于繁琐,需要自己定义
// 使用匿名类就会方便很多
method(
new Animal() {
@Override
public void eat() {
System.out.println("狗吃骨头");
}
}
);
}

public static void method(Animal a) {
a.eat();
};
}

匿名自调用

1
2
3
4
5
6
7
8
9
10
11
12
13
package InnerClass03;

public class Test3 {
public static void main(String[] args) {
new Swim() {

@Override
public void swim() {
System.out.println("重写之后的游泳方法");
}
}.swim();
}
}

综合项目————拼图

要求:

1
2
3
1. 创建一个宽603像素,高680像素的游戏主界面
2. 创建一个宽488像素,高430像素的登陆界面
3. 创建一个宽488像素,高500像素的注册界面

JFrame

Java中创建图像化窗口的JavaBean

创建JFrame对象

1
JFrame GameJFrame = new JFrame();

JFrame部分参数

1
2
3
4
5
JFrame.setSize(int width,int height);	// 设置窗口宽高
JFrame.setVisible(Boolean b); // 显示窗口,建议放到最后
JFrame.setTitle(String s); // 设置标题
JFrame.setAlwaysOnTop(Boolean b); // 设置窗口置顶
JFrame.setDefaultCloseOperation(); // 设置窗口关闭方式 参数可以填入 0-3 范围

示例:

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

import javax.swing.*;

public class GameJFrame extends JFrame {
// JFrame表示界面、窗体
// 同理它的子类也是代表界面、窗体
// GameJFrame 自定义类,表示主界面窗体
public GameJFrame(int width,int height,Boolean vb) {
this.setSize(width,height);
this.setVisible(vb);

this.setTitle("拼图单机版 v1.0"); // 设置窗口标题
this.setAlwaysOnTop(true); // 设置窗口置顶

this.setLocationRelativeTo(null); // 设置窗口居中
this.setDefaultCloseOperation(3);
}

}

利用继承,创建一个自定义类,方便后期修改和简化代码

JMenu

菜单制作流程:

  1. 先创建JMenuBar
  2. 再创建JMenu
  3. 再创建JMenuItem
  4. 把JMenuItem放到JMenu里面
  5. 把JMenu放到JMenuBar里面

创建JMenuBarJMenuJMenuItem对象:

1
2
3
JMenuBar jMenuBar = new JMenuBar();
JMenu functionJMenu = new JMenu(String s);
JMenuItem replayItem = new JMenuItem(String s);

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 初始化菜单
// 创建整个的菜单对象
JMenuBar jMenuBar = new JMenuBar();

JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");

// 创建选项下面的条目对象
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重写登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");

// 将条目添加到选项中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);

aboutJMenu.add(accountItem);

// 将菜单里面的两个选项添加到菜单当中
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);

// 将整个页面设置菜单
this.setJMenuBar(jMenuBar);

JLabel

JLabel负责管理区域,比如图片、文字

创建JLabel对象:

1
JLabel jLabel = new JLabel();

创建ImageIcon对象

1
ImageIcon icon = new ImageIcon(String s);	// s 填入图片路径

ImageIcon添加到JLabel

1
jLabel.add(icon);

指定图片位置

1
jLabel.setBounds(int x,int y,int width,int height);

JLabel添加到JFrame

1
this.getContentPane().add(jLabel);

注意

在初始化JFrame中,需要放入以下代码

1
2
// 取消默认的居中放置,只有取消了才会按照XY轴的形式添加组件
this.setLayout(null);

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void initImage() {
for (int j = 0; j < 4; j++) {
for (int i = 0; i < 4; i++) {
// 获取当前要加载的图片序号
int num = data[i][j];
// 创建一个JLabel的对象(管理容器)
JLabel jLabel = new JLabel(new ImageIcon("C://" + num + ".jpg"));
// 指定图片位置
jLabel.setBounds(105 * i,105*j,105,105);
// 将容器添加到界面中
// this.add(jLabel);
this.getContentPane().add(jLabel);
}
}

事件

  • 事件源:按钮、图片、窗体…
  • 事件:某些操作。如:鼠标单击、鼠标划入…
  • 绑定监听:当事件源上发生了某个事件,则执行某段代码
1
2
3
键盘监听:KeyListener
鼠标监听:MouseListener
动作监听:ActionListener

JButton

创建JButton对象

1
JButton jbtn = new JButton(String s);	// 按钮内的文字

设置位置、大小

1
jbtn.setBounds(int x,int y,int width,int height);

ActionListener

JButton 添加 ActionListener

第一种:

使用接口创建实现类,再将实现类添加到ActionListener中

1
2
3
4
5
public class MyActionLisntener implements ActionListener {
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击了");
}
}
1
jbtn.addActionListener(new MyActionLisntener());

第二种:

利用匿名内部类直接重写

1
2
3
4
5
6
jbtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("匿名内部类哦");
}
});

第三种(多个按钮):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class JframeButton extends JFrame implements ActionListener {
JButton btn1 = new JButton("AButton");
JButton btn2 = new JButton("BButton");

public JframeButton() {
...
btn1.setBounds(0,0,100,50);
// 这里的this代表传入本类对象,即btn1
btn1.addActionListener(this);

btn2.setBounds(200,0,100,50);
// 这里的this代表传入本类对象,即btn2
btn2.addActionListener(this);

this.getContentPane().add(btn1);
this.getContentPane().add(btn2);
...
}

public void actionPerformed(ActionEvent e) {
Object source = e.getSource();

if (source == btn1) {
System.out.println("我是第一个按钮");
} else if (source == btn2) {
System.out.println("我是第二个按钮");
}
}

}

MouseListener

使用方法和ActionListener差不多,这里只举一种例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MyMouseListener extends JFrame implements MouseListener {
JButton btn1 = new JButton("1");

public MyMouseListener() {
JFrame jfm = new JFrame();
jfm.setSize(600,500);
jfm.setAlwaysOnTop(true);
jfm.setDefaultCloseOperation(3);
jfm.setLayout(null);
jfm.setLocationRelativeTo(null);

btn1.setBounds(10,0,100,100);
btn1.addMouseListener(this);

jfm.getContentPane().add(btn1);

jfm.setVisible(true);
}

@Override
public void mouseClicked(MouseEvent e) {
Object source = e.getSource();
if (source == btn1) {
System.out.println("按下");
}
}

@Override
public void mousePressed(MouseEvent e) {
System.out.println("按住");
}

@Override
public void mouseReleased(MouseEvent e) {
System.out.println("松开");
}

@Override
public void mouseEntered(MouseEvent e) {
System.out.println("鼠标划入");
}

@Override
public void mouseExited(MouseEvent e) {
System.out.println("鼠标离开");
}
}
方法 描述
mouseClicked(MouseEvent e) 在组件上单击(按下并释放)鼠标按钮时调用
mouseEntered(MouseEvent e) 当鼠标进入组件时调用
mouseExited(MouseEvent e) 当鼠标退出组件时调用
mousePressed(MouseEvent e) 在组件上按下鼠标按钮时调用
mouseReleased(MouseEvent e) 在组件上释放鼠标按钮时调用

KeyListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyKeyListener extends JFrame implements KeyListener {

public MyKeyListener() {
this.setSize(603,680);
this.setTitle("测试按键");
this.setAlwaysOnTop(true);
this.setLocationRelativeTo(null);
this.setAlwaysOnTop(true);
this.setDefaultCloseOperation(3);
this.setLayout(null);

this.addKeyListener(this);

this.setVisible(true);
}


@Override
public void keyTyped(KeyEvent e) {

}

@Override
public void keyPressed(KeyEvent e) {
System.out.println("按下不松");
}

@Override
public void keyReleased(KeyEvent e) {
System.out.println("按下且释放");

// 获取按键编号
int code = e.getKeyCode();
System.out.println(code);
}
}
方法 描述
keyPressed(KeyEvent e) 按下键时调用
keyReleased(KeyEvent e) 当键被释放时调用
keyTyped(KeyEvent e) 键入键时调用

常用API

Math

方法名 说明
public static int abs(int a) 获取参数绝对值
public static double ceil(double a) 向上取整
public static double floor(double a) 向下取整
public static int round(float a) 四舍五入
public static int max(int a,int b) 获取两个int值中的较大值
public static double pow(double a,double b) 返回a的b次幂的值
public static double random() 返回值为double的随机值,范围[0.0,1.0]

练习一

要求:判断一个数是否为质数

方法一:

1
2
3
4
5
6
7
8
9
10
11
public static boolean primeNum(int number) {
int index = 0;
for (int i = 2; i < number; i++) {
index++;
if (number % i == 0) {
return false;
}
}
System.out.println(index);
return true;
}

缺点:循环次数过多,效率太低

方法二:

1
2
3
4
5
6
7
8
9
10
11
public static boolean primeNum(int number) {
int index = 0;
for (int i = 2; i <= Math.sqrt(number); i++) {
index++;
if (number % i == 0) {
return false;
}
}
System.out.println(index);
return true;
}

练习二

水仙花数(Narcissistic number)也被称为超完全数字不变数(pluperfect digital invariant, PPDI)、自恋数自幂数、阿姆斯壮数或阿姆斯特朗数(Armstrong number),水仙花数是指一个 3 位数,它的每个数位上的数字的 3次幂之和等于它本身。例如:1^3 + 5^3+ 3^3 = 153。

水仙花数只是自幂数的一种,严格来说3位数的3次幂数才称为水仙花数。

附:其他位数的自幂数名字

一位自幂数:独身数

三位自幂数:水仙花数

四位自幂数:四叶玫瑰数

五位自幂数:五角星数

六位自幂数:六合数

七位自幂数:北斗七星数

八位自幂数:八仙数

九位自幂数:九九重阳数

十位自幂数:十全十美数

要求:一共有多少个水仙花数

1
2
3
4
5
6
7
8
9
10
for (int i = 100; i <= 999; i++) {
int ge = i % 10;
int shi = i / 10 % 10;
int bai = i / 100 % 10;

double sum = Math.pow(ge,3) + Math.pow(shi,3) + Math.pow(bai,3);
if (sum == i) {
System.out.println(i);
}
}

System

System也是一个工具类,提供了一些与系统相关的方法

方法名 说明
public static void exit(int status) 终止当前运行的 Java 虚拟机
public static long currentTimeMillies() 返回当前系统的事件毫秒值形式
public static void arraycopy(数据源数组,起始索引,目的地数组,起始索引,拷贝个数) 数组拷贝
1
2
System.exit(0) // 虚拟机正常终止
System.out.println("我不会被打印出来");
1
2
long l = System.currentTimeMillies();
System.out.println(l)
1
2
3
4
5
int[] arr1 = {1,2,3,4,5,6,7,8,9,10};
int[] arr2 = new int[10];

// 从 arr1 数组的0索引上开始复制到 arr2 数组的0索引中,总共复制 10 个数
System.arraycopy(arr1,0,arr2,0,10);

Runtime

Runtime表示当前虚拟机的运行环境

方法名 说明
public static Runtime getRuntime() 当前系统的运行环境对象
public void exit(int status) 停止虚拟机
public int availableProcessors() 获取CPU的线程数
public long maxMemory() JVM能从系统中获取总内存大小(单位byte)
public long totalMemory() JVM已经从系统中获取总内存大小(单位byte)
public long freeMemory() JVM剩余内存大小(单位byte)
public Process exec(String Name) 运行CMD命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
Runtime r1 = Runtime.getRuntime();
Runtime r2 = Runtime.getRuntime();
System.out.println(r1 == r2);
*/

// 停止虚拟机
/*
Runtime.getRuntime().exit(0);
System.out.println("我不会被打印");
*/

// CPU线程数
System.out.println(Runtime.getRuntime().availableProcessors());

// 总内存大小
System.out.println(Runtime.getRuntime().maxMemory() / 1024 / 1024);

// 已经获取的总内存大小
System.out.println(Runtime.getRuntime().totalMemory() / 1024 / 1024);

// 剩余内存大小
System.out.println(Runtime.getRuntime().freeMemory() / 1024 / 1024);

// 执行 cmd 命令
System.out.println(Runtime.getRuntime().exec("notepad"));

Object

  • Object 是java中的顶级父类。所有的类都直接或间接的继承于Object类
  • Object 类中的方法可以被所有子类访问,所以要学习Object类和其中的方法

Object的构造方法

方法名 说明
public Object() 空参构造

Object的成员方法

方法名 说明
public String toString() 返回对象的字符串表示形式
public boolean equals(Obejct obj) 比较两个对象是否相等
protected Object clone(int a) 对象克隆

查看数据类型

1
2
3
4
5
Object obj = new Object();
Boolean flag = false;
String sflag = obj.toString();
System.out.println(flag.getClass()); // class java.lang.Boolean
System.out.println(sflag.getClass()); // class java.lang.String

toString

1
2
3
4
// OpenJDK7 java.lang.Object类toString()源码
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

也就是代表调用toString时,会返回类名 + 对象的内存地址

如果不想打印对象地址,只打印对象中的属性,那么需要在类中重写 toString()

IDEA可以直接生成如下结构

1
2
3
4
5
6
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

toString方法在很多地方都会被调用

  • 当你print一个对象的时候,会自动调用toString方法

  • 当你print一个collection的时候,其内部所持有的每个对象都会被分别调用toString方法。

  • 当你在开发环境调用debugger看类实例的时候,大部分debugger都会调用实例的toString方法为你显示其内部的信息。(例1)

  • 当你把一个对象写到log文件里的时候,会自动调用toString方法。

  • 当你用断言(Assert)检查两个实例是否相同时,会自动调用toString方法。

对象对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Student {
private String name;
private int age;

...

// 重写equals,使对象的属性之间做对比
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

Student student = (Student) o;

if (age != student.age) return false;
return Objects.equals(name, student.name);
}

@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
}
1
2
3
4
Student s1 = new Student("张三",18);
Student s2 = new Student("张三",18);
Boolean result = s1.equals(s2);
System.out.println(result); // 直接输出为 false,这是因为对比的是地址值,正确的对比需要重写equals

克隆对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class User implements Cloneable{
private int id;
private String username;
private String password;

private int[] data;

...

//

public String toString() {
return "角色编号为:" + id + ",用户名:" + username + ",密码:" + password + ",data:" + ArrtoString(data);
}

public String ArrtoString(int[] data) {
StringJoiner sj = new StringJoiner(",","[","]");
for (int i = 0; i < data.length; i++) {
sj.add(data[i] + "");
}
return sj.toString();
}

// 由于clone在Object中是受保护的,需要重写才可以使用

@Override
protected Object clone() throws CloneNotSupportedException {
// 调用父类的clone方法
// 相当于让Java帮我们克隆一个对象,并把克隆之后的对象返回出去
return super.clone();
}
}
1
2
3
4
5
6
7
int[] data = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0};
// 创建对象
User u1 = new User(1,"zhangsan","qwe123456",data);
// 克隆对象
User u2 = (User) u1.clone();
System.out.println(u1);
System.out.println(u2);

浅拷贝:

  1. 拷贝对象属性的地址值,假设 B 拷贝了 A,如果A改变了,B查看属性时也会是改变后的属性

深拷贝:

  1. 拷贝对象属性值时会重新分配地址(引用数据类型重新分配,基本数据类型不会,字符串复用)

Object clone默认为浅克隆,需要深克隆的话需要重写方法或使用第三方工具类

Objects

Objects是一个工具类,提供了一些方法去完成一些功能

方法名 说明
public static boolean equals(Object a,Object b) 先做非空判断,比较两个对象
public static boolean isNull(Object obj) 判断对象是否为Null,为Null返回true,反之
public static boolean nonNull(Object obj) 判断对象是否为Null,跟isNull的结果相反

BigInteger和BigDecimal

在Java中,整数有四种类型:byte、short、int、long

在底层占用的字节个数:byte1个字节、short2个字节、int4个字节、long8个字节

BigInteger

BigInteger 理论上存储的数字是有限制的,但是实际中,没有内存可以存储它的上限,所以可以把它看作的无上限的

BigInteger构造方法

方法名 说明
public BigInteger(int num,Random rnd) 获取随机大整数,范围:[0~2的num次方-1]
public BigInteger(String val) 获取指定大整数(字符串只能写整数)
public BigInteger(String val,int radix) 获取指定进制的大整数(radix:进制)

静态方法创建:

方法名 说明
public static BigInteger valueOf(long val) 静态方法获取BigInteger的对象,内部有优化
  1. 能表示范围比较小,只能在Long的取值范围之内
  2. 对内部常用数进行了优化(-16~16)

BigInteger常见方法

方法名 说明
public BigInteger add(BigIntger val) 加法
public BigInteger subtract(BigInteger val) 减法
public BigInteger multiply(BigInteger val) 乘法
public BigInteger divide(BigInteger val) 除法,获取商
public BigInteger[] divideAndRemainder 除法,获取商和余数
public boolean equals(Object x) 比较是否相同
public BigInteger pow(int exponent) 次幂
public BigInteger max/min(BigInteger val) 返回较大值/较小值
public int intValue(BigInteger val) 转换为int类型整数,超出数据范围则报错

BigDecimal

  • 用于小数的精确计算
  • 用来表示很大的小数

BigDecimal构造方法

方法名 说明
public BigDecimal(double val) 获取指定小数(不建议使用,小数会不精确)
public BigDecimal(String val) 获取指定小数(字符串只能写小数)

静态方法:

方法名 说明
public static BigDecimal valueOf(double val) 静态方法获取BigDecimal对象,也带有优化

如果要表示的数字不大,没有超过double的取值范围,建议使用静态方法

如果要表示的数字比较大,超过了double的取值范围,建议使用public BigDecimal(String val)

在静态方法中,如果传递的是0~10之间的整数,包含10,那么方法会返回已经创建好的对象,不会重新new一个BigDecimal对象

BigDecimal常见方法

方法名 说明
public BigDecimal add(BigDecimal val) 加法
public BigDecimal subtract(BigDecimal val) 减法
public BigDecimal multiply(BigDecimal val) 乘法
public BigDecimal divide(BigDecimal val) 除法
public BigDecimal divide(BigDecimal val, 精确几位, 舍入模式) 除法

正则表达式

正则表达式可以校验字符串是否满足一定的规则,并用来校验数据格式的合法性

需求:假如现在要求校验一个QQ号码是否正确

规则:6位及20位之间,0不能在开头,必须全是数字

字符类(只匹配一个字符)

1
2
3
4
5
6
7
[abc] 只能是a,b,c
[^abc] 除了a,b,c之外的任何字符
[a-zA-Z] a到z A到Z,包括(范围)
[a-d[m-p]] a到d,或m到p
[a-z&&[def]] a-z到def的交集。即:d,e,f
[a-z&&[^bc]] a-z和非bc的交集。即:[ad-z]
[a-z&&[^m-p]] a到z和除了m到p的交集。即:[a-lq-z]

预定义字符(只匹配一个字符)

1
2
3
4
5
6
7
. 任意字符
\d 一个数字:[0-9]
\D 非数字:[^0-9]
\s 一个空白字符:[\t\n\x0B\f\r]
\S 非空字符:[^\s]
\w 英文、数字、下划线:[a-zA-z_0-9]
\W [^\w] 一个非单词字符

数量词

1
2
3
4
5
6
x?	x,一次或0次
x* x,零次或多次
x+ x,一次或多次
x{n} x,正好n次
x{n,} x,至少n次
x{n,m} x,至少n但不超过m次

正则表达式的作用

作用1:校验字符串是否满足规则

作用2:在一段文本中查找满足要求的内容(本地爬虫、网络爬虫)

爬虫

Pattern:表示正则表达式

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
有如下文本,请按照要求爬取数据
Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是 Java8 和 Java11,
因为这两个是长期支持版本,下一个长期支持版本是 Java17,相信在未来不久 Java17 也会逐渐登上历史舞台

要求:找出里面所有的 JavaXX
*/
String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
"因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17 也会逐渐登上历史舞台";

// Pattern 表示正则表达式
// Matcher 文本匹配器,作用按照正则表达式的规则去读取字符串,从头开始读取

// 获取正则表达式的对象
// Pattern p = Pattern.compile("Java\\d{0,2}");
// Matcher m = p.matcher(str);
// 代码解释:m要在str中查找符合p规则的字符小串

// boolean b = m.find();
// 拿着文本匹配器从头开始读取,寻找是否有满足规则的字串
// 如果没有,返回false,如果有返回true,在底层记录子串的 起始索引 和 结束索引+1

// 方法底层会根据find方法记录的索引进行字符串的截取
// subString(起始索引, 结束索引); 包头不包尾,所以结束索引会+1
// String s1 = m.group();
// System.out.println(s1);

// 第二次在调用find的时候,会继续读取后面的内容
// 读取到第二个满足要求的子串,方法会继续返回true
// 并把第二个字串的起始索引和结束索引+1,进行记录
// b = m.find();
// String s2 = m.group();
// System.out.println(s2);

// 规范写法:
// 1. 获取正则表达式的对象
Pattern p = Pattern.compile("Java\\d{0,2}");
// 2. 获取文本匹配器的对象
Matcher m = p.matcher(str);

// 3. 利用循环获取
while (m.find()) {
String res = m.group();
System.out.println(res);
}

带条件的爬取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    public static void main(String[] args) {
String str = "Java自从95年问世以来,经历了很多版本,目前企业中用的最多的是Java8和Java11," +
"因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17 也会逐渐登上历史舞台";

// 1. 不显示版本号,只要java
// ? 理解为前面的数据 Java
// = 表示在Java后面要跟随的数据
// 但是在获取的时候,只获取前半部分
// String regex = "Java(?=8|11|17)";
// Pattern p = Pattern.compile(regex);
// Matcher m = p.matcher(str);
// while (m.find()) {
// System.out.println(m.group());
// }

// 2. 显示版本号
// String regex = "Java(8|11|17)";
// Pattern p = Pattern.compile(regex);
// Matcher m = p.matcher(str);
// while (m.find()) {
// System.out.println(m.group());
// }

// 3. 爬取版本号位 8,11,17 的Java
// String regex = "Java(8|11|17)";
// Pattern p = Pattern.compile(regex);
// Matcher m = p.matcher(str);
// while (m.find()) {
// System.out.println(m.group());
// }

贪婪爬起和非贪婪爬取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String str = "Java自从95年问世以来,abbbbbbbbbbbaaaaaaaaaaaaa经历了很多版本,目前企业中用的最多的是Java8和Java11," + "因为这两个是长期支持版本,下一个长期支持版本是Java17,相信在未来不久Java17 也会逐渐登上历史舞台";

// 只写+和*表示贪婪匹配,
// +? 非贪婪匹配
// *? 非贪婪匹配
// Java中默认的是贪婪爬取

// 1. 按照ab+的方式爬取ab,b尽可能多获取
// String regex = "ab+";
// Pattern p = Pattern.compile(regex);
// Matcher m = p.matcher(str);
//
// while (m.find()) {
// System.out.println( m.group());
// }

// 2. 按照ab+的方式爬取ab,b尽可能少获取
String regex = "ab+?";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(str);

while (m.find()) {
System.out.println( m.group());
}

正则表达式——字符串

方法名 说明
public String[] matches(String regex) 判断字符串是否满足正则表达式规则
public String[] replaceAll(String regex,String newStr) 按照正则表达式的规则进行替换
public String[] split(String regex) 按照正则表达式的规则切割字符串
1
2
3
4
5
6
7
8
String s = "张三三qweasdzxc123李四四poilkjmnb098王五五";
String res = s.replaceAll("[\\w&&[^_]]+","vs");
System.out.println(res);

String[] arr = s.split("[\\w&&[^_]]+");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

分组

分组就是一个小括号

1
String regex1 = "\\w+@[\\w&&[^_]]{2,6}(\\.[a-zA-Z]{2,3})"

每组是有组号的,也就是序号

规则1:从1开始,连续不间断

规则2:从左括号为基准,最左边的是第一组,其次为第二组,以此类推

捕获分组

捕获分组就是把这一组的数据捕获出来,再用一次

正则表达式内部:\\组号

正则表达式外部:$组号

1
2
3
4
5
6
7
8
9
10
11
12
// 判断一个字符串的开始字符和结束字符是否一致
String regex = "(.).+\\1";
System.out.println("a123a".matches(regex));
System.out.println("a121a".matches(regex));
System.out.println("b121a".matches(regex));
System.out.println("a123b".matches(regex));
System.out.println("------------");

// 将重复内容替换为单个
String s = "我要学学编编编编程程程程程程";
String res = s.replaceAll("(.)\\1+","$1");
System.out.println(res);

非捕获分组

分组之后不需要再用本组数据,仅仅是把数据括起来

特点:不占用组号

符号 含义 举例
(?:正则) 获取所有 Java(?:8|11|17)
(?=正则) 获取前面部分 Java(?=8|11|17)
(?!正则) 获取不是指定内容的前面部分 Java(?!8|11|17)

Date

全时间的时间,有一个统一的计算标准

格林尼治时间/格林威治时间(Greenwich Mean Time)简称GMT

计算核心:地球自转一天是24小时,太阳直射时为正午12点

缺点:误差过大

原子钟:利用铯原子的震动的频率计算出来的时间,作为世界标准时间(UTC)

中国标准时间:世界标准时间 + 8小时

时间换算单位:

  • 1秒 = 1000毫秒
  • 1毫秒 = 1000微妙
  • 1微妙 = 1000纳秒

Date时间类

Date类是一个JDK写好的Javabean类,用来描述时间,精确到毫秒。

方法名 描述
public Date() 创建Date对象,表示当前时间
public Date(long time) 创建Date对象,表示指定时间
public void setTime(long time) 设置/修改毫秒值
public long getTime() 获取时间对象的毫秒值
1
2
3
4
5
6
7
8
9
10
11
Date d1 = new Date();
System.out.println(d1);

Date d2 = new Date(0L);
System.out.println(d2);

d2.setTime(1000L);
System.out.println(d2);

long time = d2.getTime();
System.out.println(time);

SimpleDateFormat

作用:

  • 格式化:把时间变成喜欢的格式
  • 解析:把字符串表示的时间变成Date对象
方法 描述
public SimpleDateFormat() 构造一个SimpleDateFormat,使用默认格式
public SimpleDateFormat(String pattern) 构造一个SimpleDateFormat,使用指定格式
public final String format(Date date) 格式化(日期对象 -> 字符串)
public Date parse(String source) 解析(字符串 -> 日期对象)
1
2
3
4
5
6
7
8
y 年	M 月 d 日
H 时 m 分 s 秒

2025-06-15 23:42:10
yyyy-MM-dd HH:mm:ss

2025年06月15日 23:42:10
yyyy年MM月dd日 HH:mm:ss

Calendar

  • Calendar 代表了系统当前时间的日历对象,可以单独修改、获取时间中的年、月、日
  • 细节:Calendar 是一个抽象类,不能直接创建对象
方法名 说明
public static Calendar getInstance() 获取当前时间的日历对象
public final Date getTime() 获取日期对象
public final setTime(Date date) 给日历设置日历对象
public long getTimeInMillies() 拿到时间毫秒值
public void setTimeInMillies() 给日历设置时间毫秒值
public int get(int field) 取日历中的某个字段信息
public void set(int field,int value) 修改日历的某个字段信息
public void add(int field,int amount) 为某个字段增加/减少指定的值

JDK8新增时间类

代码层面:

  • JDK7:代码麻烦
  • JDK8:简单

安全层面:

  • JDK7:多线程环境下会导致数据安全的问题
  • JDK8:时间日期对象都是不可变的,解决了这个问题
ZoneId 时区
Instant 时间戳
ZoneDateTime 带时区的时间
DateTimeFormatter 用于时间的格式化和解析
LocalDate 年月日
LocalTime 时分秒
LocalDateTime 年月日时分秒
Duration 时间间隔(秒。纳秒)
Period 时间间隔(年月日)
ChronoUnit 时间间隔(所有单位)

包装类

包装类:基本数据类型对应的引用类型

基本数据类型 包装类
byte Byte
short Short
char Character
int Integer
long Long
float Float
double Double
boolean Boolean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 利用构造方法获取Integer(JDK5以前的方式)
Integer i1 = new Integer(1);

// 利用静态方法获取Integer(JDK5以前的方式)
Integer i2 = Integer.valueOf(2);
Integer i3 = Integer.valueOf("525",16);

System.out.println(i2);
System.out.println(i3);

// 这两种方式获取对象的区别
// 和之前学习到的BigInteger一样,内部进行优化,127~-128 提前创建对象,不会重复创建新的对象
Integer i4 = Integer.valueOf(127);
Integer i5 = Integer.valueOf(127);
System.out.println(i4 == i5); // true

Integer i6 = Integer.valueOf(128);
Integer i7 = Integer.valueOf(128);
System.out.println(i6 == i7); // true


// 因为有new关键字,在Java中,每一次new都是创建了一个新的对象
// 所以下面的两个对象都是new出来的,地址值不一样
Integer i8 = new Integer(127);
Integer i9 = new Integer(127);
System.out.println(i8 == i9); // true

Integer i10 = new Integer(128);
Integer i11 = new Integer(128);
System.out.println(i10 == i11); // true

自动装箱/自动拆箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 在以前包装类的计算方式
Integer i1 = new Integer(1);
Integer i2 = new Integer(2);

// 对 i1 与 i2 进行相加操作
// 1. 把对象进行拆箱,变成基本数据类型
// 2. 相加
// 3. 把得到的结果再次进行装箱(再变回包装类)
// int res = i1.intValue() + i2.intValue();
// Integer i3 = new Integer(res);
// System.out.println(i3);

// 在JDK5的时候提出了一个机制,自动装箱和自动拆箱
// 自动装箱:把基本数据类型会自动的变成其对应的包装类
// 自动拆箱:把包装类自动的变成其对象的基本数据类型

// 自动装箱动作
Integer i5 = 10;
Integer i6 = new Integer(20);

// 自动拆箱动作
int i = i2;

// 在JDK5以后,int和Integer可以看作是同一个东西,因为在内部可以自动转化

Integer成员方法

方法名 说明
public static String toBinaryString(int i) 得到二进制
public static String toOctalString(int i) 得到八进制
public static String toHexString(int i) 得到十六进制
public static int parseInt(String s) 将字符串类型的整数转成int类型的整

集合进阶

集合体系结构

Collection:单列集合

单列集合:即一次只能添加一组数据

Map:双列集合

双列集合:即一次可以添加一对数据

flowchart TB
  Start["Collection"]
  Start -->List
  Start -->Set
  
  List --> id1[(ArrayList)]
  List --> id2[(LinkedList)]
  List --> id3[(Vector)]
  
  Set --> id4[(HashSet)]
  Set --> id5[(TreeSet)]
  
  id4[(HashSet)] --> id6[(LinkedHashSet)]
  

  接口
  id7[(实现类)]

List系列集合:添加的元素是有序(存和取的顺序是一样的)、可重复、有索引

Set系列集合:添加的元素是无序、不重复、无索引

单列集合Collection

Collection是单列集合的顶层接口,它的功能是全部单列集合都可以继承使用的

方法名称 说明
public boolean add(E e) 把给定的对象添加到当前集合中
public void clear() 清空集合中所有的元素
public boolean remove(E e) 把给定的对象在当前集合中删除
public boolean contains(Object obj) 判断当前集合是否包含给定的对象
public boolean isEmpty() 判断当前集合是否为空
public int size() 返回集合中元素的个数/集合的长度

Collection的遍历方式

  • 迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历

迭代器遍历

迭代器在Java中的类是Iterator,迭代器是集合专用的遍历方式

Collection集合

方法名称 说明
Iterator<e> iterator() 返回迭代器对象,默认指向当前集合的0索引

Iterator中的常用方法

方法名称 说明
boolean hasNext() 判断当前位置是否有元素,有元素返回true,没有元素返回false
E next() 获取当前位置的元素,并将迭代器对象移向下一个位置
1
2
3
4
5
6
7
8
9
10
11
12
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
coll.add("eee");
coll.add("eee");

Iterator<String> it = coll.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

细节注意点:

  1. 当迭代器迭代完所有数据后,再强行调用 next() 方法,会报 NoSuchElementExpection
  2. 迭代器遍历完毕,指针不会复位
  3. 循环中只能用一次 next() 方法
  4. 迭代器遍历时,不能用集合的方法进行增加或者删除

增强for遍历

  • 增强for的底层就是迭代器,为了简化迭代器的代码书写的
  • 它是JDK5之后出现的,其内部原理就是一个Iterator迭代器
  • 所有的单列集合和数组才能用增强for进行遍历

格式:

1
2
3
for (元素的数据类型 变量名:数组或者集合) {

}
1
2
3
for (String s : list) {
System.out.println(s);
}

示例:

1
2
3
4
5
6
7
8
9
10
11
// 1. 创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add("wangwu");
coll.add("zhaoliu");

// 利用增强for循环
for (String s : coll) {
System.out.println(s);
}

细节:

  • 修改增强for中的变量,不会改变集合中原本的数据
1
2
3
for (String s : coll) {
s = "qqq"; // 并不会修改值
}

Lambda表达式遍历

得益于JDK8开始的新技术Lambda表达式,提供了一种更简单、更直接的遍历集合的方式

方法名称 说明
default void forEach(Consumer<? super T> action) 结合lambda遍历集合

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 创建集合并添加元素
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add("wangwu");
coll.add("zhaoliu");

// 2. 利用匿名内部类的形式
// forEach底层原理:
// 也会自己遍历集合,依次得到每一个元素
// s依次表示集合中的每一个数据
coll.forEach(new Consumer<String>() {
@Override
// s 依次表示集合中的每一个数据
public void accept(String s) {
System.out.println(s);
}
});

// lambda表达式
coll.forEach(s -> System.out.println(s));

选择哪种遍历方式

  • 迭代器:在遍历的过程中需要删除元素,选择迭代器
  • 仅仅遍历,可以使用增强for或lambda

List

特点:

  • 有序:存和取的元素顺序一致
  • 有索引:可以通过索引操作元素
  • 可重复:存储的元素可以重复
  • List继承了Collection的所有方法
  • List因为有索引,所以多了很多索引操作的方法
方法名 说明
void add(int index,E element) 在此集合中的指定位置插入指定的元素
E remove(int index) 删除指定索引的元素,返回删除的元素
E set(int index,E element) 修改指定索引处的元素,返回被修改的元素
E get(int index) 返回指定索引处的元素

List遍历

  • 迭代器遍历
  • 列表迭代器遍历
  • 增强for遍历
  • Lambda表达式遍历
  • 普通for循环(因为List集合存在索引)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");

// 1. 迭代器
// Iterator<String> it = list.iterator();
// while (it.hasNext()) {
// System.out.println(it.next());
// }

// 2. 增强for
// for (String s : list) {
// System.out.println(s);
// }

// 3. lambda
// list.forEach(s -> System.out.println(s));

// 4. 普通for循环
// for (int i = 0; i < list.size(); i++) {
// System.out.println(list.get(i));
// }

// 5. 列表迭代器
// 获取一个列表迭代器的对象,里面的指针默认指向0索引
ListIterator<String> it = list.listIterator();
while (it.hasNext()) {
// 迭代器可以添加元素
String s = it.next();
if("bbb".equals(s)) {
it.add("qqq");
}
}

System.out.println(list);
while (it.hasPrevious()) {
System.out.println(it.previous());
}
选择遍历方式

迭代器遍历:在遍历过程中需要删除元素,使用迭代器

列表迭代器:在遍历的过程中需要添加元素,使用列表迭代器

增强for遍历:仅仅想遍历,使用增强for或Lambda遍历

Lambda遍历:仅仅想遍历,使用增强for或Lambda遍历

普通for遍历:想要操作索引,使用普通for遍历

ArrayList集合底层原理

  1. ArrayLIst底层是数组结构
  2. 利用空参创建的集合,在底层创建一个默认长度为0的数组
  3. 添加第一个元素时,底层会创建一个新的长度为10的数组(数组名为elementData,变量size记录数组中元素的个数以及下一个元素存放的位置)
  4. 存满数组时,会扩容1.5倍
  5. 如果一次添加多个元素,1.5倍放不下,则新创建数组的长度以实际为准

LinkedList集合

  1. 底层数据结构是双链表,查询慢,增删快,但是如果操作的是首尾元素,速度也是极快的
  2. LinkedList本身多了很多直接操作首尾元素的特有API
特有方法 说明
public void addFirst(E e) 在该列表开头插入指定的元素
public void addLast(E e) 将指定的元素追加到此列表的末尾
public E getFirst() 返回此列表中的第一个元素
public E getLast() 返回此列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素

Set

添加的元素是无序、不重复、无索引

  • 无序:存取顺序不一致
  • 不重复:可以去除重复值
  • 无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素

Set集合的实现类

  • HashSet:无序、不重复、无索引
  • LinkedHashSet:有序、不重复、无索引
  • TreeSet:可排序、不重复、无索引

Set接口中的方法基本上与Collection的API一致

Set遍历方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.function.Consumer;

public class A13_SetDemo1 {
public static void main(String[] args) {
// 1. 创建一个set对象
Set<String> s = new HashSet<>();

// 2. 添加元素
// 如果添加的元素集合中不存在,返回值为true
// 如果添加的元素集合中存在,返回值为false
s.add("张三");
s.add("李四");
s.add("王五");

System.out.println(s);


// 迭代器遍历
System.out.println("----迭代器----");
Iterator<String> it = s.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

// 增强for遍历
System.out.println("----增强for----");
for (String s1 : s) {
System.out.println(s1);
}

// lambda表达式
// forEach遍历
System.out.println("----forEach----");
s.forEach(s1 -> {
System.out.println(s1);
});
}
}

HashSet

底层原理:

  • HashSet集合底层采取哈希表存储数据
  • 哈希表是一种对于增删改查数据性能都较好的结构

哈希表组成:

  • JDK8之前:数组+链表
  • JDK8开始:数组+链表+红黑树
哈希值
  • 根据hashCode方法算出来的int类型的整数
  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
  • 一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值
对象的哈希值特点
  • 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
  • 如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
  • 在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)
JDK8前的底层原理
  1. 创建一个默认长度为16,默认加载因子为0.75的数组,数组名table
  2. 根据元素的哈希值跟数组的长度计算出应存入的位置

公式:

1
int index = (数组长度 - 1) & 哈希值;
  1. 判断当前位置是否为null,如果是null直接存入
  2. 如果位置不为null,表示有元素,则调用equals方法比较属性值
  3. 一样:不存 不一样:存入数组,形成链表(去重)

JDK8以前:新元素存入数组,老元素挂在新元素下面

JDK8以后:新元素直接挂在老元素下面,当链表长度大于8且数组长度大于等于64,自动形成红黑树

LinkedHashSet

  • 有序、不重复、无索引
  • 这里的有序指的是保证存储和取出的元素顺序一致
  • 原理:底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序

TreeSet

  • 不重复、无索引、可排序
  • 可排序:按照元素的默认规则(从小到大)排序
  • TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package mySet;

import java.util.Iterator;
import java.util.TreeSet;

public class A16_TreeSetDemo1 {
public static void main(String[] args) {
/*
需求:利用 TreeSet 存储整数并排序
*/

TreeSet<Integer> ts = new TreeSet<>();
ts.add(1);
ts.add(3);
ts.add(5);
ts.add(2);
ts.add(4);
ts.add(6);

System.out.println(ts);

// 迭代器遍历
System.out.println("迭代器遍历:");
Iterator<Integer> it = ts.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}

// 增强for遍历
System.out.println("增强for遍历:");
for (Integer t : ts) {
System.out.println(t);
}

// forEach遍历
System.out.println("forEach遍历:");
ts.forEach(v -> {
System.out.println(v);
});
}
}
排序规则
  • 对于数值类型:Integer、Double,默认按照从小到大的顺序进行排序
  • 对于字符类型:按照字符在ASCII码表中的数字升序进行排序
  • 字符串:首先比较首位字符的ASCII码,若相同,则比较后一位
TreeSet的两种比较方式

方式一:

默认排序/自然排序:Javabean类实现Comparable接口指定比较规则

方式二:

比较器排序:创建TreeSet对象的时候,传递比较器Comparator指定规则

使用原则:

默认使用第一种,如果第一种不能满足当前需求,就使用它第二种

双列集合Map

双列集合的特点

  • 双列集合一次需要存一对数据,分别为键和值
  • 键不能重复,值可以重复
  • 键和值是一一对应的,每一个键只能找到自己对应的值
  • 键+值这个整体,称之为“键值对”或者“键值对对象”,在Java中叫做“Entry对象”
flowchart TD
    Map[Map]
    Map --> HashMap[HashMap]
    Map --> Hashtable[Hashtable]
    Hashtable --> Properties[Properties]
    HashMap --> LinkedHashMap[LinkedHashMap]
    Map --> TreeMap[TreeMap]

Map的常见API

Map是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的

方法名称 说明
V put(K key,V value) 添加元素
V remove(Object key) 根据键删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object Value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数

示例:

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

public class A01_MapDemo1 {
public static void main(String[] args) {
// 1. 创建Map集合的对象
Map<String,String> m = new HashMap<>();

// 2. 添加元素
// 添加数据时,键不存在,则会把键值对添加到Map中,返回值为null
// 添加数据时,键存在,则会覆盖原来的键值对,返回值为被覆盖的值
m.put("张三","13岁");
m.put("李四","14岁");
m.put("王五","13岁");

String val = m.put("张三","14岁");

System.out.println(val); // 13岁
System.out.println(m);


// 3. 删除元素
String res = m.remove("张三");
System.out.println(res); // 14岁

// 4. 清空集合
// m.clear();

// 5. 判断包含内容
System.out.println(m);
if (m.containsKey("张三")) {
System.out.println("有一个叫张三的人");
} else if (m.containsKey("李四")) {
System.out.println("有一个叫李四的人");
}

if (m.containsValue("14岁")) {
System.out.println("有一个14岁的人");
}

// 6. 判断集合是否为空
// m.clear();
if (m.isEmpty()) {
System.out.println("集合为空");
} else {
System.out.println("集合不为空");
}

// 7. 查看集合的大小
System.out.println("集合中元素的个数为:" + m.size());
}
}

Map遍历

键找值

  1. 利用 keySet()方法,获取集合中的所有值,返回值为一个单列集合
  2. 遍历单列集合,得到每一个键
  3. 在遍历体中,使用get(Object key)方法获取键对应的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class A02_MapDemo2 {
public static void main(String[] args) {
// 遍历Map集合
Map<String, String> hs = new HashMap<>();
hs.put("张三", "18");
hs.put("李四", "13");
hs.put("王五", "15");
hs.put("赵六", "20");
hs.put("路人甲", "17");
hs.put("路人乙", "16");
hs.put("路人丙", "21");
hs.put("路人丁", "18");

// 1. 通过键找值
// 1.1 获取所有的键,把这些键放到一个单列集合中
Set<String> keys = hs.keySet();

// 1.2 遍历单列集合,得到每一个键
for (String key : keys) {
// 1.3 利用键获取对应的值
System.out.println(key + "=" + hs.get(key));
}
}
}

键值对

  1. 通过entrySet()方法,获取所有的键值对对象,方法会返回一个Set集合,Set集合中存放的是键值对对象
  2. 遍历entries集合,获取每一个键值对对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class A03_MapDemo3 {
public static void main(String[] args) {
// 遍历Map集合
Map<String, String> hs = new HashMap<>();
hs.put("张三", "18");
hs.put("李四", "13");
hs.put("王五", "15");
hs.put("赵六", "20");
hs.put("路人甲", "17");
hs.put("路人乙", "16");
hs.put("路人丙", "21");
hs.put("路人丁", "18");

// 1. 通过键值对对象进行遍历
// 1.1 通过entrySet()方法,获取所有的键值对对象,方法会返回一个Set集合,Set集合中存放的是键值对对象
Set<Map.Entry<String, String>> entries = hs.entrySet();

// 1.2 遍历entries集合,获取每一个键值对对象
for (Map.Entry<String, String> entry : entries) {
// System.out.println(entry); // 直接打印
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "=" + value);
}

}
}

Lambda表达式

方法名 说明
default void forEach(BiConsumer<? super K,? super V> action) 结合lambda遍历Map集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

public class A04_MapDemo4 {
public static void main(String[] args) {
// 遍历Map集合
Map<String, String> hs = new HashMap<>();
hs.put("张三", "18");
hs.put("李四", "13");
hs.put("王五", "15");
hs.put("赵六", "20");
hs.put("路人甲", "17");
hs.put("路人乙", "16");
hs.put("路人丙", "21");
hs.put("路人丁", "18");

// 1. 利用lambda遍历
/*
* hs.forEach(new BiConsumer<String, String>() {
* @Override
* public void accept(String key, String value) {
* System.out.println(key + "=" + value);
* }
* });
*/

hs.forEach((key,value) -> {
System.out.println(key + "=" + value);
});
}
}

HashMap

特点:

  1. HashMap是Map里面的一个实现类
  2. 没有额外需要学习的特有方法,直接使用Map里面的方法
  3. 特点都是由键决定的:无序、不重复、无索引
  4. HashMap跟HashSet底层原理是一摸一样的,都是哈希表结构

细节:

  1. HashMap底层是哈希表结构的
  2. 依赖hashCode方法和equals方法保证键的唯一
  3. 如果键存储的是自定义对象,需要重写hashCode和equals方法。
  4. 如果值存储的是自定义对象,不需要重写hashCode和equals方法

LinkedHashMap

  • 由键决定:有序、不重复、无索引
  • 这里的有序指的是保证存储和取出的元素顺序一致
  • 原理:底层结构依然是哈希表,只是每个键值对元素又额外的多了一个双链表的机制记录存储顺序

TreeMap

  • TreeMap跟TreeSet底层原理一样,都是红黑树结构
  • 由键决定特性:不重复、无索引、可排序
  • 可排序:对键进行排序
  • 注意:默认按照键的从小到大进行排序,也可以自己规定键的排序

Collections

  • java.util.Collections:是集合工具类
  • 作用:Collections不是集合,而是集合的工具类

常用API

方法名称 说明
public static boolean addAll(Collection c,T…elements) 批量添加元素
public static void shuffle(List<?> list) 打乱List集合元素的顺序
public static void sort(List list) 排序
public static void sort(List list,Comparator c) 根据指定的规则进行排序
public static int binarySearch(List list,T key) 以二分查找法查找元素
public static copy(List dest,List src) 拷贝集合中的元素
public static void fill(List list,T obj) 使用指定的元素填充集合
public static max/min(Collection coll) 根据默认的自然排序获取最大/最小值
public static void swap(List<?> list,int i,int j) 交换集合中指定位置的元素

泛型

泛型:是JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查

泛型的格式:<数据类型>

注意:泛型只能支持引用数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class A07_GenericsDemo1 {
public static void main(String[] args) {
// 当集合没有指定数据类型时,默认所有的元素都是Object
// 此时可以往集合添加任意的数据类型
// 带来一个坏处:我们在获取数据的时候,无法使用他的特有行为
ArrayList list = new ArrayList();
list.add(123);
list.add("abc");
list.add(new Student(18,"张三"));


}
}

class Student{
public int age;
public String name;

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

泛型带来的好处:

  • 同一数据类型
  • 把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的问题,因为在编译阶段就能确定下来

扩展知识点:Java中的泛型是伪泛型

解释:编写Java文件时(*.java),泛型是负责约束数据类型的,但在编译为机器码后(*.class),会去除泛型,将数据类型变为Object。这一过程称为泛型擦除

泛型的细节:

  • 泛型中不能写基本数据类型
  • 指定泛型的具体类型后,传递数据时,可以传入该类类型或者子类类型
  • 如果不写泛型,类型默认为Object

泛型可以在很多地方进行定义:

  • 类后面(ArraryList<数据类型>):泛型类
  • 方法上面:泛型方法
  • 接口后面:泛型接口

泛型类

使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类

格式:

1
2
3
修饰符 class 类名<类型> {

}

举例:

1
2
3
public class ArrayList<E> {

}

此处的E可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V等

具体代码:

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

public class A08_GenericsDemo2 {
public static void main(String[] args) {
MyArrayList<Integer> list = new MyArrayList<>();

list.add(111);
list.add(222);
list.add(333);

System.out.println(list.get(0));
System.out.println(list.get(1));
System.out.println(list.get(2));

System.out.println("-------------");
System.out.println(list.toString());
}
}

class MyArrayList<E> {
Object[] obj = new Object[10];
int size;

/*
E : 表示不确定的类型
e : 表示形参名,变量名
*/
public boolean add(E e) {
obj[size] = e;
size++;
return true;
}

public E get(int index) {
return (E)obj[index];
}

@Override
public String toString() {
return Arrays.toString(obj);
}
}

泛型方法

方法中形参类型不确定时:

方案一:使用类名后面定义的泛型(所有的方法都可以使用)

1
2
3
4
5
6
7
class MyArrayList<E> {
public boolean add(E e) {
obj[size] = e;
size++;
return true;
}
}

方案二:在方法申明上定义自己的泛型(只能在本方法中使用)

public <E> boolean add(E e) {
    obj[size] = e;
    size++;
    return true;
}

格式:

1
2
3
修饰符 <类型> 返回值类型 方法名(类型 变量名) {

}

泛型接口

格式:

1
2
3
修饰符 interface 接口名<类型> {

}

泛型接口的两种使用方式:

  1. 实现类给出具体的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

public class A09_GenericsDemo3 {
public static void main(String[] args) {
MyArrayList2 list = new MyArrayList2();
list.add("abc");
// list.add(123); // 报错,因为添加的是整数类型,而该对象只能添加字符串类型
}
}

class MyArrayList2 implements List<String> {

@Override
public int size() {
return 0;
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public boolean contains(Object o) {
return false;
}

@Override
public Iterator<String> iterator() {
return null;
}

@Override
public Object[] toArray() {
return new Object[0];
}

@Override
public <T> T[] toArray(T[] a) {
return null;
}

@Override
public boolean add(String s) {
return false;
}

@Override
public boolean remove(Object o) {
return false;
}

@Override
public boolean containsAll(Collection<?> c) {
return false;
}

@Override
public boolean addAll(Collection<? extends String> c) {
return false;
}

@Override
public boolean addAll(int index, Collection<? extends String> c) {
return false;
}

@Override
public boolean removeAll(Collection<?> c) {
return false;
}

@Override
public boolean retainAll(Collection<?> c) {
return false;
}

@Override
public void clear() {

}

@Override
public String get(int index) {
return null;
}

@Override
public String set(int index, String element) {
return null;
}

@Override
public void add(int index, String element) {

}

@Override
public String remove(int index) {
return null;
}

@Override
public int indexOf(Object o) {
return 0;
}

@Override
public int lastIndexOf(Object o) {
return 0;
}

@Override
public ListIterator<String> listIterator() {
return null;
}

@Override
public ListIterator<String> listIterator(int index) {
return null;
}

@Override
public List<String> subList(int fromIndex, int toIndex) {
return null;
}
}
  1. 实现类延续泛型,创建实现类对象时再确定类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

public class A10_GenericsDemo4 {
public static void main(String[] args) {
MyArrayList3<Integer> list = new MyArrayList3<>();
list.add(123);

MyArrayList<String> slist = new MyArrayList<>();
slist.add("abc");
}
}

class MyArrayList3<E> implements List<E> {

@Override
public int size() {
return 0;
}

@Override
public boolean isEmpty() {
return false;
}

@Override
public boolean contains(Object o) {
return false;
}

@Override
public Iterator<E> iterator() {
return null;
}

@Override
public Object[] toArray() {
return new Object[0];
}

@Override
public <T> T[] toArray(T[] a) {
return null;
}

@Override
public boolean add(E e) {
return false;
}

@Override
public boolean remove(Object o) {
return false;
}

@Override
public boolean containsAll(Collection<?> c) {
return false;
}

@Override
public boolean addAll(Collection<? extends E> c) {
return false;
}

@Override
public boolean addAll(int index, Collection<? extends E> c) {
return false;
}

@Override
public boolean removeAll(Collection<?> c) {
return false;
}

@Override
public boolean retainAll(Collection<?> c) {
return false;
}

@Override
public void clear() {

}

@Override
public E get(int index) {
return null;
}

@Override
public E set(int index, E element) {
return null;
}

@Override
public void add(int index, E element) {

}

@Override
public E remove(int index) {
return null;
}

@Override
public int indexOf(Object o) {
return 0;
}

@Override
public int lastIndexOf(Object o) {
return 0;
}

@Override
public ListIterator<E> listIterator() {
return null;
}

@Override
public ListIterator<E> listIterator(int index) {
return null;
}

@Override
public List<E> subList(int fromIndex, int toIndex) {
return null;
}
}

泛型的继承和通配符

  • 泛型不具备继承性,但是数据具备继承性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;

public class A11_GenericsDemo5{
public static void main(String[] args) {
// 泛型不具备继承性,但是数据具备继承性
ArrayList<Fu> list1 = new ArrayList<>();
ArrayList<Zi> list2 = new ArrayList<>();

// method(list1); // 集合的数据类型为Fu,所以调用method方法时,和形参的数据类型匹配,不会报错
// method(list2); // 集合的数据类型为Zi,所以调用method方法时,和形参的数据类型不匹配,会报错

// 数据可以具备继承性
list1.add(new Fu());
list1.add(new Zi());
}

public static void method(ArrayList<Fu> list) {

}
}

class Fu{};

class Zi extends Fu{};

如果将method方法改为:

1
2
3
public static <E> void method(ArrayList<E> list) {

}

会有一个弊端,就是所有的数据类型都可以接受

但本方法虽然不确定类型,但只希望传递Fu、Zi

可以使用泛型的通配符

通配符

泛型的通配符有两种:

  • ? extends E
  • ? super E

第一个表示可以传递E或者E所有的子类类型

第二个表示可以传递E或者E所有的父类类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.ArrayList;

public class A12_GenericsDemo6{
public static void main(String[] args) {
// 泛型不具备继承性,但是数据具备继承性
ArrayList<Fu> list1 = new ArrayList<>();
ArrayList<Zi> list2 = new ArrayList<>();

method(list1);
method(list2);
}

public static void method(ArrayList<? extends Fu> list) {

}
}

class Fu{};

class Zi extends Fu{};

可变参数

方法形参的个数是可以改变的。

注意点:

  1. 在方法的形参中最多只能写一个可变参数
  2. 在方法中,如果除了可变参数以外,还有其他的形参,那么可变参数要写在最后

格式:形参类型…形参名

如:

1
int...args

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ArgsDemo1 {
public static void main(String[] args) {
// 创建一个方法,可以计算两个数的和
// 创建一个方法,可以计算三个数的和
// 创建一个方法,可以计算四个数的和
// 创建一个方法,可以计算n个数的和

// 第一种:
// 此时如果要计算n个数的和,就需要有n个参数的方法
System.out.println(getSum(10,20));
System.out.println(getSum(10,20,30));
System.out.println(getSum(10,20,30,40));
}
public static int getSum(int a,int b) {
return a + b;
}

public static int getSum(int a,int b,int c) {
return a + b + c;
}

public static int getSum(int a,int b,int c,int d) {
return a + b + c + d;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ArgsDemo2 {
public static void main(String[] args) {
// 创建一个方法,可以计算两个数的和
// 创建一个方法,可以计算三个数的和
// 创建一个方法,可以计算四个数的和
// 创建一个方法,可以计算n个数的和

// 第二种:
// 虽然这种方法可以计算n个数的和,但是使用起来相对复杂
int[] arr = {10,20,30,40,50};
int sum = getSum(arr);
System.out.println(sum);
}
public static int getSum(int[] arr) {
int sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ArgsDemo3 {
public static void main(String[] args) {
// 创建一个方法,可以计算两个数的和
// 创建一个方法,可以计算三个数的和
// 创建一个方法,可以计算四个数的和
// 创建一个方法,可以计算n个数的和

// 从JDK5开始,有了可变参数
// 可变参数:方法形参的个数是个发生变化的
// 格式:属性类型...形参名
// int...args

System.out.println(getSum(10,20,30,40,50));
}

// 底层:
// 可变参数底层就是一个数组
// 只不过不需要使用者自己创建
public static int getSum(int...args) {
int sum = 0;
for (int i = 0; i < args.length; i++) {
sum += args[i];
}
return sum;
}
}

应用场景:

  1. 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口
  2. 如果类型不确定,但是能知道以后只能传递某个继承体系中的,就可以用泛型的通配符

不可变集合

不可变集合:不可以被修改的集合(长度、内容)

  • 如果某个数据不能被修改,把它防御性的拷贝到不可变集合中是个很好的实践
  • 当集合对象被不可信的库调用时,不可变形式是安全的

简单理解:

不想让别人修改集合中的内容

书写格式

在List、Set、Map接口中,都存在静态的of方法,可以获取一个不可变的集合

方法名称 说明
static list of(E…elements) 创建一个具有指定元素的List集合对象
static Set of(E…elements) 创建一个具有指定元素的Set集合对象
static Map of(E…elements) 创建一个具有指定元素的Map集合对象

注意1:这个集合不能添加、不能删除、不能修改

注意2:Map.of()方法内参数最多为20个(10个键值对),如果要添加大于10个键值对的参数,先创建map可变集合,再用Map.copyOf()将可变集合复制给不可变集合

Stream流

Stream流的作用

结合了Lambda表达式,简化集合、数组的操作

Stream流的使用步骤

  1. 先得到一条Stream流(流水线),并把数据放上去
  2. 利用Stream流中的API进行各种操作
获取方式 方法名 说明
单列集合 default Stream stream() Collection中的默认方法
双列集合 无法直接使用stream流
数组 public static Stream stream(T[] array) Arrays工具类中的静态方法
一堆零散数据 public static Stream of(T…values) Stream接口中的静态方法

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class StreamDemo3 {
public static void main(String[] args) {
// 1. 单列集合获取stream流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"a","b","c","d","e");
list.stream().forEach(s -> System.out.println(s));

// 2. 双列集合
HashMap<String,Integer> hm = new HashMap<>();
hm.put("aaa",111);
hm.put("bbb",222);
hm.put("ccc",333);
hm.put("ddd",444);

// 第一种: ketSet方法获取stream流
hm.keySet().stream().forEach(s -> System.out.println(s + "=" + hm.get(s)));

// 第二种:entrySet方法获取stream流
hm.entrySet().stream().forEach(s-> System.out.println(s));

// 3. 数组获取Stream流
int[] arr1 = {1,2,3,4,5,6};
String[] arr2 = {"abc","bcd","a","b","c"};
Arrays.stream(arr1).forEach(s -> System.out.println(s));
Arrays.stream(arr2).forEach(s -> System.out.println(s));

// 4. 零散数据
Stream.of("a","b",3,4,5).forEach(s -> System.out.println(s));
}
}

Stream流的中间方法

名称 说明
Stream filter(Predicate<? superT> predicate) 过滤
Stream limit(long maxSize) 获取前几个元素
Stream skip(long n) 跳过前几个元素
Stream distinct() 元素去重,依赖(hashCode和equals方法)
static Stream concat(Stream a,Stream b) 合并a和b两个流为一个流
Stream map(Function<T,R> mapper) 转换流中的数据类型

注意1:中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程

注意2:修改Stream流中的数据,不会影响原来集合或者数组中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StreamDemo5 {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list1,"bob-15","alan-20","john-10","bob-15","bob-15","alan-20");
Collections.addAll(list2,"aaa","bbb","aaa","ccc","ddd","eee");

// distinct 去重
// list1.stream().distinct().forEach(s -> System.out.println(s));

// concat 合并流
// Stream.concat(list1.stream(),list2.stream()).forEach(s -> System.out.println(s));

// map 转换流中的数据类型
// s 表示转换后的数据
/*
list1.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
// 获取字符串中的数字
String[] arr = s.split("-");
String ageString = arr[1];
int age = Integer.parseInt(ageString);
return age;
}
}).forEach(s -> System.out.println(s));
**/

list1.stream().map(s -> Integer.parseInt(s.split("-")[1])).forEach(s -> System.out.println(s));
}
}

Stream流的终结方法

名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中

前三个方法使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class StreamDemo6 {
public static void main(String[] args) {
// 遍历
Stream.of("1","2","3","4","5","6","7").forEach(s -> System.out.println(s));

// 统计
long count = Stream.of("1", "2", "3", "4", "5", "6", "7").count();
System.out.println("count:" + count);

// toArray:创造一个指定类型的数组
// 第一种使用方法:
Object[] objects = Stream.of("1", "2", "3", "4", "5", "6", "7").toArray();
System.out.println(Arrays.toString(objects));

// 第二种使用方法:
String[] str = Stream.of("1", "2", "3", "4", "5", "6", "7").toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});

System.out.println(Arrays.toString(str));

// 第三种使用方法:
String[] strings = Stream.of("1", "2", "3", "4", "5", "6", "7").toArray(value -> new String[value]);
System.out.println(Arrays.toString(strings));
}
}

collect案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class StreamDemo7 {
public static void main(String[] args) {
// collect:收集流中的数据,放到集合中(List、Set、Map)
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张三-男-15","李四-男-14","王五-男-16","王若男-女-14","赵丽-女-15","张强-男-14","刘莉莉-女-13","陈念-男-15","龚月月-女-14");

// 收集到List集合当中
// 需求:
// 把所有的男性收集起来
List<String> newlist = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toList());
System.out.println(newlist);

// 收集到Set集合当中
// 需求:
// 把所有的男性收集起来
Set<String> newset = list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toSet());
System.out.println(newset);

// 收集Map集合当中
// 需求:
// 键:名字,值:年龄
// toMap方法中有两个参数,都是方法接口
// 第一个参数是键的值,方法中泛型的第一个类型是流里的数据类型,第二个类型是Map集合键的数据类型
// 第二个参数是值的值,用法和第一个一样
list.stream().filter(s -> "男".equals(s.split("-")[1])).collect(Collectors.toMap(s -> s.split("-")[0],s -> s.split("-")[2])).entrySet().stream().forEach(s -> System.out.println(s));
}
}

方法引用(略看一遍以后再说)

方法:就是以前学习的方法

引用:把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体

方法引用:把已经有的方法拿过来用,当作函数式接口中抽象方法的方法体

引用符号:::

例如:

1
2
3
4
5
6
Arrays,sort(arr,new Comparator<Integer>() {
@override
public int compare(Integer o1,Integer o2) {
return o1 - o2;
}
})

可以变成如下形式:

假设有一个写好的方法:

1
2
3
public int subtraction(int n1,int n2) {
return n1 - n2;
}
1
2
Arrays.sort(arr,FunctionDemo::subtraction);
// FunctionDemo::subtraction:表示在FunctionDemo中有一个方法叫subtraction

注意点:

  1. 引用出必须是函数式接口
  2. 被引用的方法必须已经存在
  3. 被引用的方法的形参和返回值需要跟抽象方法保持一致
  4. 被引用的方法需要满足当前的需求

方法引用的分类

  1. 引用静态方法
  2. 引用成员方法
    • 引用其他类的成员方法
    • 引用本类的成员方法
    • 引用父类的成员方法
  3. 引用构造方法

引用静态方法

格式:类名::静态方法

范例:

1
Integer::parseInt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FunctionDemo1 {
public static void main(String[] args) {
// 集合中有以下数字,要求把它们都变成int类型
// "1","2","3","4","5"

// 1. 创建集合
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"1","2","3","4","5");

/*list.stream().map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
int num = Integer.parseInt(s);
return num;
}
}).forEach(s -> System.out.println(s));*/

list.stream().map(Integer::parseInt).forEach(System.out::println);

}
}

引用成员方法

格式:对象::成员方法

  1. 其他类:其他类对象::方法名
  2. 本类:this::方法名
  3. 父类:super::方法名

引用构造方法

格式:类名::new

范例:Student::new

其他调用方式

  1. 使用类名引用成员方法
    • 范例:String::substring
  2. 引用数组的构造方法
    • 范例:int[]::new

异常

异常:异常就是代表程序出现的问题

注意:异常的作用不是让我们以后不出现异常,而是程序出了异常之后,该如何处理

flowchart TD
    top[Java.lang.Throwable]
    top --> midleft[Error]
    top --> midright[Exception]
    midright --> bottomleft[RuntimeException]
    midright --> bottomright[其他异常]
    bottomleft --> other["。。。"]

Error:代表的是系统级别错误(属于严重问题),系统一旦出现问题,sun公司会把这些错误封装成Error对象。

Error是给sun公司自己用的,不是给我们程序员用的。

因此我们开发人员不用管他。

Exception:叫做异常,代表程序可能出现的问题。我们通常会用Exception以及他的子类来封装程序出现的问题。

运行时异常:RuntimeException及其子类,编译阶段不会出现异常提醒。运行时出现的异常(如:数组索引越界异常)

编译时异常:编译阶段就会出现异常提醒的。(如:日期解析异常)

flowchart TD
    top[Java文件]
    top -- Javac命令 --> mid[字节码文件]
    mid -- Java命令 --> bottom[运行结果]

Java文件到字节码文件为编译时异常(如:日期解析异常)

字节码文件到运行结果为运行时异常,RuntimeException及其子类,编译阶段不需要处理。代码运行时出现的异常(如:数组索引越界异常)

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ExceptionDemo1 {
public static void main(String[] args) throws ParseException {
// 编译时异常(在编译阶段,必须要手动处理,否则代码报错)
/*String time = "2030年1月1日";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
Date date = sdf.parse(time);
System.out.println(date);*/

// 运行时异常
int[] arr = {1,2,3,4,5};
System.out.println(arr[10]); // ArrayIndexOutOfBoundsException
}
}

编译时异常和运行时异常的区别

  • 编译时异常:除了RuntimeException和它的子类,其他都是编译时异常。编译阶段需要进行处理,作用在于提醒程序员
  • 运行时异常:RuntimeException本生和所有子类,都是运行时异常。编译阶段不报错,是程序运行时出现的。一般是由于参数传递错误带来的问题

异常的作用

作用一:异常是用来查询bug的关键参考信息

作用二:异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ExceptionDemo2 {
public static void main(String[] args){
// 作用一:异常是用来查询bug的关键参考信息
// 案例一:
// Student[] students = new Student[3];
// String name = students[0].getName();
// System.out.println(name); // NullPointerException

// 案例二:
// Student zs = new Student("张三,23");
// System.out.println(zs);

// 作用二:异常可以作为方法内部的一种特殊返回值,以便通知调用者底层的执行情况
// 年龄范围(18 - 40)
Student s1 = new Student();
s1.setAge(50);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Student {
private String name;
private int age;

public Student() {};

public Student(String str) {
String[] arr = str.split("-");
this.name = arr[0];
this.age = Integer.parseInt(arr[1]);
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
if (age < 18 || age > 40) {
throw new RuntimeException();
} else {
this.age = age;
}
}
}

异常的处理方式

  1. JVM默认的处理方式
  2. 自己处理
  3. 抛出异常

JVM默认的处理方式

  • 把异常的名称,异常原因及异常出现的位置等信息输出在了控制台
  • 程序停止执行,下面的代码就不会执行了

自己处理(捕获异常)

格式:

1
2
3
4
5
try {
可能出现异常的代码;
} catch {
异常的处理代码
}

目的:当代码出现异常时,可以让程序继续往下执行

示例:

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

public class ExceptionDemo4 {
public static void main(String[] args){
/*自己处理(捕获异常)
格式:
try {
可能出现异常的代码;
} catch {
异常的处理代码
}*/

int[] arr = {1,2,3,4,5};
try{
// 可能出现异常的代码
System.out.println(arr[10]);
// 如果此处出现了异常,程序就会在这里创建一个ArrayIndexOutOfBoundsException对象
// new ArrayIndexOutOfBoundsException();
// 拿着这个对象到catch的小括号中对比,看括号中的变量是否可以接收这个对象
// 如果能被接收,就表示该异常被捕获(抓住),执行catch里面的内容
} catch(ArrayIndexOutOfBoundsException e) {
// 如果出现了ArrayIndexOutOfBoundsException该如何处理
System.out.println("索引越界了");
}

System.out.println("我会被执行");
}
}

问题一:如果try中没有遇到问题,怎么执行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExceptionDemo5 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};

// 如果try中没有遇到问题,怎么执行?
try{
System.out.println(arr[0]); // 执行该行代码
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界了");
}

System.out.println("我被执行了"); // 执行该行代码

}
}

问题二:如果try中可能遇到多个问题,怎么执行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class ExceptionDemo6 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};

// 如果try中可能遇到多个问题,怎么执行?
// 写多个catch与之对应
// 细节:如果要捕获多个异常,如果这些异常存在父子关系的话,那么父类一定要写在下面
// 在JDK之后,可以在catch中同时捕获多个异常,中间用 | 进行隔开
/*try{
System.out.println(arr[10]);
System.out.println(2/0);
String s = null;
System.out.println(s.equals("abc"));
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("索引越界了"); // 执行改行代码
} catch (ArithmeticException e) {
System.out.println("除数不能为0");
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (Exception e) {
System.out.println("Exception");
}*/
try{
System.out.println(arr[10]);
System.out.println(2/0);
String s = null;
System.out.println(s.equals("abc"));
} catch (ArrayIndexOutOfBoundsException | ArithmeticException | NullPointerException e) {

}


System.out.println("我被执行了"); // 执行该行代码

}
}

问题三:如果try中遇到的问题没有被捕获,怎么执行?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExceptionDemo7 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
// 如果try中遇到的问题没有被捕获,怎么执行?
// 如果try中遇到的问题没有捕获(没有匹配到异常),就相当于try...catch的代码白写了
// 最终会交给虚拟机进行处理
try{
System.out.println(arr[10]);
}catch (NullPointerException e) {
System.out.println("空指针异常");
}

System.out.println("我不会被执行");
}
}

问题四:如果try中遇到了问题,那么try下面的其他代码还会执行吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExceptionDemo8 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
// 如果try中遇到了问题,那么try下面的其他代码还会执行吗?
try{
System.out.println(arr[10]);
System.out.println("我不会被执行");
}catch (ArrayIndexOutOfBoundsException e) {
System.out.println("空指针异常");
}

System.out.println("我会被执行");
}
}

Throwable的成员方法

方法名称 说明
public String getMessage() 返回此throwable的详细消息字符串
public String toString() 返回此可抛出的简短描述
public void printStackTrace() 把异常的错误信息输出在控制台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package myexception;
public class ExceptionDemo9 {
public static void main(String[] args) {
/*| public String getMessage() | 返回此throwable的详细消息字符串 |
| public String toString() | 返回此可抛出的简短描述 |
| public void printStackTrace() | 把异常的错误信息输出在控制台 |*/

int[] arr = {1,2,3,4,5};
try {
System.out.println(arr[10]);
} catch (ArrayIndexOutOfBoundsException e) {
// String message = e.getMessage();
// System.out.println(message); // Index 10 out of bounds for length 5

// String s = e.toString();
// System.out.println(s); // java.lang.ArrayIndexOutOfBoundsException: Index 10 out of bounds for length 5

e.printStackTrace(); // 类似与JVM的默认处理方式(红色字体打印异常),但是不会结束虚拟机
}

System.out.println("我是否被执行了?");
}
}

抛出处理

throws

注意:写在方法定义处,表示声明一个异常,告诉调用者,使用本方法可能有哪些异常

格式:

1
2
3
public void 方法()throws 异常类名1,异常类名2... {
...
}
  • 编译时异常:必须要写
  • 运行时异常:可以不写

throw

注意:写在方法内,结束方法。手动抛出异常对象,交给调用者。方法中下面的代码不再执行了

示例:

1
2
3
public void 方法() {
throw new NullPointerException();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package myexception;
public class ExceptionDemo10 {
public static void main(String[] args) {
// 需求:定义一个方法求数组最大值
int[] arr = null;
int max = 0;
try {
max = getMax(arr);
} catch (NullPointerException e) {
System.out.println("空指针异常");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("数组超出索引");
}
System.out.println("我还是会打印");
}

public static int getMax(int[] arr)/* throws NullPointerException,ArrayIndexOutOfBoundsException `RuntimeException的子类` */ {
if (arr == null) {
// 手动创建一个异常对象,并把这个异常交给方法的调用者处理
// 此时方法就会结束,下面的代码就不会再执行了
throw new NullPointerException();
}

if (arr.length == 0) {
throw new ArrayIndexOutOfBoundsException();
}

int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (max < arr[i]) {
max = arr[i];
}
}
return max;
}
}

自定义异常

  1. 定义异常类
  2. 写继承关系
  3. 空参构造
  4. 带参构造

意义:就是为了让控制台的报错信息更加的见名知意

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

import java.util.InputMismatchException;
import java.util.Scanner;

public class ExceptionPractice2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);

while (true) {
try {
System.out.println("请输入长度在3-10之间的姓名:");
String name = sc.nextLine();
if (name.length() < 3 || name.length() > 10) {
throw new NameFormatException(name + "格式有误,长度应为:3~10");
}

System.out.println("请输入18-40之间的年龄:");
int age = sc.nextInt();
sc.nextLine(); // 清除输入流中的换行符

if (age < 18 || age > 40) {
throw new AgeFormatException(age + "格式有误,范围应为:18~40");
}

break; // 如果姓名和年龄都合法,就跳出循环
} catch (InputMismatchException e) {
System.out.println("年龄异常,请输入一个整数");
sc.nextLine(); // 清除输入流中的错误数据
} catch (NameFormatException e) {
e.printStackTrace();
sc.nextLine();
} catch (AgeFormatException e) {
e.printStackTrace();
sc.nextLine();
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package myexception;

import javax.naming.Name;

public class NameFormatException extends RuntimeException{
// 技巧:
// NameFormat:当前异常的名字,表示姓名格式化问题
// Exception:表示当前类是一个异常类

// 运行时:RuntimeException 核心 就表示由于参数错误而导致的
// 编译时:Exception 核心 提醒程序员检查本地信息

public NameFormatException() {};

public NameFormatException(String message) {
super(message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package myexception;

public class AgeFormatException extends RuntimeException{
// 技巧:
// NameFormat:当前异常的名字,表示姓名格式化问题
// Exception:表示当前类是一个异常类

// 运行时:RuntimeException 核心 就表示由于参数错误而导致的
// 编译时:Exception 核心 提醒程序员检查本地信息

public AgeFormatException() {};

public AgeFormatException(String message) {
super(message);
}
}

File

  • File对象就表示一个路径,可以是文件的路径、也可以是文件夹的路径
  • 这个路径可以是存在的,也允许是不存在
方法名称 说明
public File(String pathname) 根据文件路径创建文件对象
public File(String parent,String child) 根据父路径名字符串和子路径名字符串创建文件对象
public File(File parent,String child) 根据父路径对应文件对象和子路径字符串创建文件对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.File;

public class FileDemo1 {
public static void main(String[] args) {
// 1. public File(String pathname)
// 根据文件路径创建文件对象
String str = "C:\\Users\\Zhao\\Desktop\\a.txt";
File f1 = new File(str);
System.out.println(f1); // C:\Users\Zhao\Desktop\a.txt

// 2. public File(String parent,String child)
// 父路径:C:\Users\Zhao\Desktop
// 子路径:a.txt
String parent = "C:\\Users\\Zhao\\Desktop";
String child = "a.txt";
File f2 = new File(parent,child);
System.out.println(f2); // C:\Users\Zhao\Desktop\a.txt


// 3. public File(File parent,String child)
// 把一个File表示的路径和String表示的路径进行拼接
File parent2 = new File("C:\\Users\\Zhao\\Desktop");
String child2 = "a.txt";
File f3 = new File(parent2,child);
System.out.println(f3); // C:\Users\Zhao\Desktop\a.txt
}
}

File的常见成员方法

判断、获取

方法名称 说明
public boolean isDirectory() 判断此路径名表示的File是否为文件夹
public boolean isFile() 判断此路径名表示的File是否为文件
public boolean exists() 判断此路径名表示的File是否存在
public long length() 返回文件的大小(字节数量)
public String getAbsolutePath() 返回文件的绝对路径
public String getPath() 返回定义文件时使用的路径
public String getName() 返回文件的名称,带后缀
public long lastModified() 返回文件的最后修改时间(时间毫秒值)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.File;

public class FileDemo2 {
public static void main(String[] args) {
/*
| 方法名称 | 说明 |
| ---------------------------- | ---------------------------------- |
| public boolean isDirectory() | 判断此路径名表示的File是否为文件夹 |
| public boolean isFile() | 判断此路径名表示的File是否为文件 |
| public boolean exists() | 判断此路径名表示的File是否存在 |
* */

// 1. 对一个文件的路径进行判断
File f1 = new File("E:\\data\\a.txt");
System.out.println(f1.isDirectory()); // false
System.out.println(f1.isFile()); // true
System.out.println(f1.exists()); // true
System.out.println("-------------");


// 2. 对一个文件夹的路径进行判断
File f2 = new File("E:\\data\\bbb");
System.out.println(f2.isDirectory()); // true
System.out.println(f2.isFile()); // false
System.out.println(f2.exists()); // true
System.out.println("-------------");

// 3. 对一个不存在的路径进行判断
File f3 = new File("E:\\data\\c.txt");
System.out.println(f3.isDirectory()); // false
System.out.println(f3.isFile()); // false
System.out.println(f3.exists()); // false
System.out.println("-------------");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.io.File;

public class FileDemo3 {
public static void main(String[] args) {
/*
| 方法名称 | 说明 |
| ------------------------------- | ------------------------------------ |
| public long length() | 返回文件的大小(字节数量) |
| public String getAbsolutePath() | 返回文件的绝对路径 |
| public String getPath() | 返回定义文件时使用的路径 |
| public String getName() | 返回文件的名称,带后缀 |
| public long lastModified() | 返回文件的最后修改时间(时间毫秒值) |
* */

// 1. length 返回文件的大小(字节数量)
// 无法获取文件夹的大小,只能获取文件的大小,单位:字节
File f1 = new File("E:\\data\\a.txt");
File f2 = new File("E:\\data\\bbb");
long length1 = f1.length();
long length2 = f2.length();
System.out.println(length1);
System.out.println(length2);

// 2. getAbsolutePath 文件绝对路径
File f3 = new File("a.txt");
File absoluteFile1 = f1.getAbsoluteFile();
File absoluteFile2 = f3.getAbsoluteFile();
System.out.println(absoluteFile1);
System.out.println(absoluteFile2);

// 3. getPath
String path1 = f1.getPath();
String path2 = f3.getPath();
System.out.println(path1);
System.out.println(path2);

// 4. getName
// 如果路径是文件的路径,返回 文件名 + 后缀名
// 如果路径是文件夹,返回文件夹名
String name1 = f1.getName();
String name2 = f3.getName();
System.out.println(name1);
System.out.println(name2);

// 5. lastModified
long l = f3.lastModified();
System.out.println(l);
}
}

创建、删除

方法名称 说明
public boolean createNewFile() 创建一个新的空的文件
public boolean mkdir() 创建单级文件夹
public boolean mkdirs() 创建多级文件夹
public boolean delete() 删除文件、空文件夹

delete方法默认只能删除文件和空文件夹,delete方法直接删除不走回收站

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

public class FileDemo4 {
public static void main(String[] args) throws IOException {
/*
| 方法名称 | 说明 |
| ------------------------------ | -------------------- |
| public boolean createNewFile() | 创建一个新的空的文件 |
| public boolean mkdir() | 创建单级文件夹 |
| public boolean mkdirs() | 创建多级文件夹 |
| public boolean delete() | 删除文件、空文件夹 |
* */

// 1. createNewFile
// 当前路径表示的文件不存在,则创建成功,方法返回 true
// 当前路径表示的文件存在,则创建失败,方法返回 false
// 如果父级路径不存在,方法会有异常(IOException)
// 该方法创建的一定是文件,如果路径中不包含后缀名,则创建一个没有后缀的文件
File f1 = new File("E:\\test\\a.txt"); // 假设该文件不存在
boolean newFile = f1.createNewFile();
System.out.println(newFile); // true

// 2. mkdir
// windows当中路径是唯一的,如果当前路径已经存在,则创建失败,返回 false
// 该方法只能创建单级文件夹,无法创建多级文件夹
File f2 = new File("E:\\test\\aaa"); // 假设该文件夹不存在
File f3 = new File("E:\\test\\bbb\\bbb\\bbb"); // 假设该文件夹不存在
boolean mkdir1 = f2.mkdir();
boolean mkdir2 = f3.mkdir();
System.out.println(mkdir1); // true
System.out.println(mkdir2); // false

// 3. mkdirs
// 既可以创建单级文件夹,也可以创建多级文件夹
File f4 = new File("E:\\test\\bbb\\bbb\\bbb"); // 假设该文件夹不存在
File f5 = new File("E:\\test\\ccc"); // 假设该文件夹不存在
boolean mkdirs1 = f4.mkdirs();
boolean mkdirs2 = f5.mkdirs();
System.out.println(mkdirs1); // true
System.out.println(mkdirs2); // true

// 4. delete
File f6 = new File("E:\\test\\a.txt"); // 存在
File f7 = new File("E:\\test\\aaa"); // 空文件夹
File f8 = new File("E:\\test\\bbb\\bbb\\bbb"); // 多级文件夹
boolean delete1 = f6.delete();
boolean delete2 = f7.delete();
boolean delete3 = f8.delete();
System.out.println(delete1); // true
System.out.println(delete2); // true
System.out.println(delete3); // false
}
}

遍历

方法名称 说明
public static File[] listRoots() 列出可用的文件系统根
public String[] list() 获取当前该路径下所有内容
public String[] list(FilenameFilter filter) 利用文件名过滤器获取当前该路径下所有内容
public File[] listFiles() 获取当前该路径下所有内容
public File[] listFiles(FileFilter filter) 利用文件名过滤器获取当前该路径下所有内容
public File[] listFiles(FilenameFilter filter) 利用文件名过滤器获取当前该路径下所有内容

listFiles

  • 当调用者File表示的路径不存在,返回Null
  • 当调用者File表示的路径是文件时,返回Null
  • 当调用者File表示的路径是一个空文件夹时,返回一个长度为0的数组
  • 当调用者File表示的路径是一个有内容的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回
  • 当调用者File表示的路径是一个有隐藏文件的文件夹时,将里面所有文件和文件夹的路径放在File数组中返回,包括隐藏文件
  • 当调用者File表示的路径是需要权限才能访问的文件夹时,返回Null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package myFile;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.Arrays;

public class FileDemo5 {
public static void main(String[] args) throws IOException {
/*
| 方法名称 | 说明 |
| ---------------------------------------------- | ---------------------------------------- |
| public static File[] listRoots() | 列出可用的文件系统根 |
| public String[] list() | 获取当前该路径下所有内容 |
| public String[] list(FilenameFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
| public File[] listFiles() | 获取当前该路径下所有内容 |
| public File[] listFiles(FileFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
| public File[] listFiles(FilenameFilter filter) | 利用文件名过滤器获取当前该路径下所有内容 |
* */

// 1. listFiles 获取指定路径下所有文件和文件夹的路径
File f1 = new File("E:\\");

File[] files = f1.listFiles();
for (File file : files) {
System.out.println(file);
}

// 2. listRoots
File[] filesRoots = File.listRoots();
System.out.println(Arrays.toString(filesRoots)); // [C:\, E:\],表示系统存在C盘和E盘

// 3. list 获取指定路径下所有内容(仅能获取名字)
File f2 = new File("E:\\");
String[] list = f2.list();
for (String s : list) {
System.out.println(s);
}

// 4. list(FilenameFilter filter) 利用文件名过滤器获取当前该路径下所有内容
// 获取 E:\\data 文件夹里面所有 txt 文件
// accept方法的形参,依次表示aaa文件夹里面每一个文件或者文件夹的路径
// 参数一:父级路径
// 参数二:子级路径
// 如果返回值为true,就表示当前路径保留
// 如果返回值为false,就表示当前路径舍弃
File f3 = new File("E:\\data");
String[] list1 = f3.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
File src = new File(dir, name);
return src.isFile() && name.endsWith(".txt");
}
});
System.out.println(Arrays.toString(list1));
}
}

IO流

存储和读取数据的解决方案

File类只能对文件本身进行操作,不能读写文件里面存储的数据

IO流用于读写文件中的数据(可以读写文件,或网络中的数据)

flowchart TD
    top[IO流]
    top --> mid1[输入流]
    top --> mid2[输出流]
    mid1 --> bottom1[读取]
    mid2 --> bottom2[写出]
flowchart TD
    top[IO流]
    top --> mid1[字节流]
    top --> mid2[字符流]
    mid1 --> bottom1[所有类型的文件]
    mid2 --> bottom2[纯文本文件]

IO流的体系

flowchart TD
    top[IO流体系]
    top --> midleft[字节流]
    top --> midright[字符流]
    midleft --> bottom1left[InputStream--字节输入流]
    midleft --> bottom1right[OutputStream--字节输出流]
    midright --> bottom2left[Reader--字节输入流]
    midright --> bottom2right[Writer--字符输出流]

InputStream、OutputStream、Reader、Writer都为抽象类

基本流:

  • FileInputStream
  • FileOutputStream
  • FileReader
  • FileWriter

FileOutPutStream

操作本地文件的字节输出流,可以把程序中的数据写到本地文件中

步骤:

  1. 创建字节输出流对象
1
2
3
4
// 细节1:参数是字符串表示的路径或者是File对象都是可以的
// 细节2:如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的
// 细节3:如果文件已经存在,则会清空文件(覆盖)
FileOutputStream fos = new FileOutputStream("a.txt");
  1. 写数据
1
2
// 细节:write方法的参数是整数,但是实际上写到本地文件的是整数在ASCII上对应的字符
fos.write(97);
  1. 释放资源
1
2
// 每次使用完流之后都要释放资源
fos.close();

FileOutPutStream写数据的3种方式

方法名称 说明
void write(int b) 一次写一个字节数据
void write(byte[] b) 一次写一个字节数组数据
void write(byte[] b,int off,int len) 一次写一个字节数组的部分数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamDemo2 {
public static void main(String[] args) throws IOException {
/*
| 方法名称 | 说明 |
| ------------------------------------ | ---------------------------- |
| void write(int b) | 一次写一个字节数据 |
| void write(byte[] b) | 一次写一个字节数组数据 |
| void write(byte[] b,int off,int len) | 一次写一个字节数组的部分数据 |*/
// 1.创建对象
FileOutputStream fos = new FileOutputStream("a.txt");

// 2. 写出数据
fos.write(97); // a
fos.write(98); // b

byte[] bytes = {97,98,99,100,101}; // abcde
fos.write(bytes);

fos.write(bytes,1,3); // 索引 1~3,即 98、99、100
// 3. 释放资源
fos.close();

}
}

FileOutPutStream写数据的两个问题

问题一:换行

问题二:续写

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

public class ByteStreamDemo3 {
public static void main(String[] args) throws IOException {
/*
* 换行写:
* 再次写出一个换行符就可以了
* windows:\r\n
* linux: \n
* max: \r
* 细节:在windows操作系统中,java对回车换行进行了优化
* 虽然完整的是 \r\n ,但是只需要写其中一个 \r 或者 \n,
* java也可以实现换行,因为java会在底层补全。
* 但是还是建议写全
*
* 续写:
* 如果想要续写,打开续写开关即可
* 开关位置:创建对象的第二个参数
* 默认false,表示关闭续写,此时创建对象会清空文件
* 手动传递true,表示打开续写,此时创建对象不会清空文件
* */


// 1. 创建对象
FileOutputStream fos = new FileOutputStream("a.txt",true);

// 2. 写出数据
String str = "zheshiyiduanceshiwenzi";
byte[] bytes = str.getBytes();
fos.write(bytes);

// 写出一个换行符就可以实现换行效果
String warp = "\r\n";
byte[] warpbytes = warp.getBytes();
fos.write(warpbytes);


String str2 = "huanhang";
fos.write(str2.getBytes());
// 3. 释放资源
fos.close();
}
}

FileInputStream

操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来

书写步骤

  1. 创建字节输入流对象
  2. 读数据
  3. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package mybytestream;

import java.io.FileInputStream;
import java.io.IOException;

public class ByteStreamDemo4 {
public static void main(String[] args) throws IOException {
/*
* 操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来
* 书写步骤
* 1. 创建字节输入流对象
* 细节1:如果文件不存在,就直接报错
* 这和字节输出流不一样,输出流遇到不存在的文件,会自动创建
*
* 2. 读数据
* 细节2:一次读一个字节,读出来的是数据在ASCII上对应的数字
* 细节3:读到文件末尾了,read方法返回-1
*
* 3. 释放资源
**/

// 1. 创建对象
FileInputStream fis = new FileInputStream("b.txt");

// 2. 读取数据
int b1 = fis.read(); // 一次只能读取一个字节
System.out.println(b1 + ":" + (char)b1);

int b2 = fis.read(); // 一次只能读取一个字节
System.out.println(b2 + ":" + (char)b2);

int b3 = fis.read(); // 一次只能读取一个字节
System.out.println(b3 + ":" + (char)b3);

int b4 = fis.read(); // 一次只能读取一个字节
System.out.println(b4 + ":" + (char)b4);

int b5 = fis.read(); // 一次只能读取一个字节
System.out.println(b5 + ":" + (char)b5);

int b6 = fis.read(); // 读取不到数据了,结果为 -1
System.out.println(b6);


// 3. 释放资源
fis.close();
}
}

FileInputStream循环读取

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

import java.io.FileInputStream;
import java.io.IOException;

public class ByteStreamDemo5 {
public static void main(String[] args) throws IOException {
// 字节输入流循环读取

// 1. 创建对象
FileInputStream fis = new FileInputStream("b.txt");

// 2. 循环读取
int b;
while ((b = fis.read()) != -1) {
System.out.println((char)b);
}

// 3. 释放资源
fis.close();
}
}
1
2
3
4
// 如果写成这种形式,read方法在循环中会执行两次,导致结果错误
while ((fis.read()) != -1) {
System.out.println((fis.read());
}

FileInputStream一次读多个字节

方法名称 说明
public int read() 一次读一个字节数据
public int read(byte[] buffer) 一次读一个字节数组数据

注意:一次读一个字节数组的数据,每次读取会尽可能把数组装满

轻量文件拷贝

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamDemo6 {
public static void main(String[] args) throws IOException {
// 拷贝文件

// 1. 创建对象
FileInputStream fis = new FileInputStream("E:\\copy\\1.jpg");
FileOutputStream fos = new FileOutputStream("E:\\copy\\bak.jpg");

// 2. 拷贝
// 核心思想:边读边写
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}

// 3. 释放资源
// 规则:先开的最后关闭
fos.close();
fis.close();
}
}

大文件拷贝

IO流:如果拷贝的文件过大,速度会变慢。

这是由于拷贝文件时,一次读取一个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;

public class ByteStreamDemo7 {
public static void main(String[] args) throws IOException {
// public int read(byte[] buffer) 一次读一个字节数组数据

// 1. 创建对象
FileInputStream fis = new FileInputStream("E:\\copy\\1.jpg");
FileOutputStream fos = new FileOutputStream("E:\\copy\\bak.jpg");

// 2. 拷贝
/*
// 一次读一个字节数据;
long timeBefore = System.currentTimeMillis();
int b;
while ((b = fis.read()) != -1) {
fos.write(b);
}
long timeAfter = System.currentTimeMillis();
System.out.println("拷贝文件花费了" + (timeAfter - timeBefore)); // 78
**/

// 一次读一个字节数组数据
long timeBefore = System.currentTimeMillis();
byte[] bytes = new byte[1024 * 1024 * 5];
int len; // 这个变量用于记录读取到了几个数据
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes,0,len);
}
long timeAfter = System.currentTimeMillis();
System.out.println("拷贝文件花费了" + (timeAfter - timeBefore)); // 9
// 3. 释放资源
// 规则:先开的最后关闭
fos.close();
fis.close();
}
}

try…catch异常处理

1
2
3
4
5
6
7
try {
...
} catch() {
...
} finally {
...
}

特点:finally里面的代码一定被执行,除非虚拟机停止

基本做法:

1
2
3
4
5
6
7
8
try{
FileOutputStream fos = new FileOutputStream("a.txt")'
fos.write(97);
} catch (IOException e) {
e.printStackTrace();
} finally {
fos.close();
}

手动释放资源

示例:

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class ByteStreamDemo8 {
public static void main(String[] args){
// 1. 创建对象
FileInputStream fis = null;
FileOutputStream fos = null;

try {
fis = new FileInputStream("E:\\copy\\1.jpg");
fos = new FileOutputStream("E:\\copy\\bak.jpg");

// 2. 拷贝
byte[] bytes = new byte[1024 * 1024 * 5];
int len;
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes,0,len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3. 释放资源
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

JDK7方案(AutoCloseable)

特点:特定情况下,可以自动释放资源

1
2
3
4
5
try(创建流对象1,创建流对象2) {
可能出现异常的代码;
} catch(异常类名 变量名) {
异常的处理代码;
}

资源用完最终自动释放

JDK9方案(AutoCloseable)

1
2
3
4
5
6
7
8
创建流对象1;
创建流对象2;

try(流1,流2) {
可能出现异常的代码;
} catch(异常类名 变量名) {
异常的处理代码;
}

资源用完最终自动释放

字符集

字节流读取文件的时候,文件中不要有中文

ASCII字符集

flowchart TD
    id1["a[要查询的英文]"]
    id1 --查询ASCII--> id2["97(110 0001)[对应的数字]"]
    id2 --编码--> id3["0110 0001"]
    id3 --解码--> id4[97]
    id4 --查询ASCII--> id5["a[读取到的英文]"]

计算机的存储规则

  1. GB2312字符集:1980年发布,1981年5月1日实施ide简体中文汉字编码国家标准。收录7445个图形字符,其中包括6763个简体汉字
  2. BIG5字符集:台湾地区繁体中文标准字符集,共收录13053个中文字,1984年实施。
  3. GBK字符集:2000年3月17日发布,收录21003个汉字。包括国家标准GB13000-1中的全部中日韩汉字,和BIG5编码中的所有汉字(完全兼容ASCII)
  4. Unicode字符集:国际标准字符集,他将世界各种语言的每个字符定义一个唯一的编码,以满足跨语言、跨平台的文本信息转换

windows系统默认使用的就是GBK

GBK

英文

flowchart TD
    id1["a[要查询的英文]"]
    id1 --查询GBK--> id2["97(110 0001)[对应的数字]"]
    id2 --编码--> id3["0110 0001"]
    id3 --解码--> id4[97]
    id4 --查询GBK--> id5["a[读取到的英文]"]

中文

flowchart TD
    id1["汉[要查询的汉字]"]
    id1 --查询GBK--> id2["47802(10111010 10111010)[对应的数字]"]
    id2 --编码--> id3["10111010 10111010"]
    id3 --解码--> id4[47802]
    id4 --查询GBK--> id5["汉[读取到的汉字]"]

规则1:汉字两个字节存储

规则2:高位字节(即两个字节的第一个字节)二进制一定以1开头,转成十进制之后是一个负数

Unicode

英文

flowchart TD
    id1["a[要查询的英文]"]
    id1 --查询Unicode--> id2["97[对应的数字]"]
    id2 --编码--> id3["00000000 01100001"]
    id3 --解码--> id4[97]
    id4 --查询Unicode--> id5["a[读取到的英文]"]

UTF-8编码规则:用1~4个字节保存

UTF-8编码方式:

  • 0xxxxxxx
  • 110xxxxx 10xxxxxx
  • 1110xxxx 10xxxxxx 10xxxxxx
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

UTF-16编码规则:用2~4个字节保存

UTF-32编码规则:固定使用四个字节保存

中文

flowchart TD
    id1["汉[要查询的中文]"]
    id1 --查询Unicode--> id2["27721(01101100 01001001)[对应的数字]"]
    id2 --编码--> id3["11100110 10110001 10001001"]

乱码

原因1:读取数据时未读完整个汉字

原因2:编码和解码时的方式不统一

如何不产生乱码

  1. 不要用字节流读取文本文件
  2. 编码解码时使用同一个码表,同一个编码方式

Java编码的方法

String类中的方法 说明
public byte[] getBytes() 使用默认方式进行编码
public byte[] getBytes(String charsetName) 使用指定方式进行编码

Java解码的方法

String类中的方法 说明
String(byte[] bytes) 使用默认方式进行解码
String(byte[] bytes,String charsetName) 使用指定方式进行解码

示例:

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

public class charsetDemo1 {
public static void main(String[] args) throws UnsupportedEncodingException {
/*
* Java编码方法
* | String类中的方法 | 说明 |
* | ------------------------------------------ | -------------------- |
* | public byte[] getBytes() | 使用默认方式进行编码 |
* | public byte[] getBytes(String charsetName) | 使用指定方式进行编码 |
*
* Java解码方法
* | String类中的方法 | 说明 |
* | --------------------------------------- | -------------------- |
* | String(byte[] bytes) | 使用默认方式进行解码 |
* | String(byte[] bytes,String charsetName) | 使用指定方式进行解码 |
* */

// 1. 编码
// 默认方式
String str = "Hello世界";
byte[] bytes1 = str.getBytes();
System.out.println(Arrays.toString(bytes1));

// 指定编码方式
byte[] bytes2 = str.getBytes("GBK");
System.out.println(Arrays.toString(bytes2));

// 2. 解码
String str2 = new String(bytes1);
System.out.println(str2);
String str3 = new String(bytes2); // 如果使用默认解码方式,会有乱码,因为编码时采用的是GBK
System.out.println(str3);
String str4 = new String(bytes2,"GBK");
System.out.println(str4);
}
}

字符流

字符流的底层其实就是字节流

字符流 = 字节流 + 字符集

特点:

输入流:一次读一个字节,遇到中文时,一次读多个字节

输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中

使用场景:

对于纯文本文件进行读写操作

FileReader

  1. 创建字符输入流对象
构造方法 说明
public FileReader(File file) 创建字符输入流关联本地文件
public FileReader(String pathname) 创建字符输入流关联本地文件

细节1:如果文件不存在,就直接报错

  1. 读取数据
成员方法 说明
public int read() 读取数据,读到末尾返回-1
public int read(char[] buffer) 读取多个数据,读到末尾返回-1

细节1:按字节进行读取,遇到中文,一次读多个字节,读取后解码,返回一个整数

细节2:读到文件末尾了 ,read方法返回-1

  1. 释放资源
成员方法 说明
public int close() 释放资源/关流

示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.FileReader;
import java.io.IOException;

public class CharStreamDemo1 {
public static void main(String[] args) throws IOException {
// 1. 创建对象
FileReader fr = new FileReader("c.txt");

// 2. 读取数据
int ch;
while ((ch = fr.read()) != -1) {
System.out.print((char)ch);
}

// 3. 释放资源
fr.close();
}
}

示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.FileReader;
import java.io.IOException;

public class CharStreamDemo2 {
public static void main(String[] args) throws IOException {
// 1. 创建对象
FileReader fr = new FileReader("c.txt");

// 2. 读取数据
char[] chars = new char[2];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars,0,len));
}

// 3. 释放资源
fr.close();
}
}

FileWrite

  1. 创建字符输出流对象
构造方法 说明
public FileWriter(File file) 创建字符输出流关联本地文件
public FileWriter(String pathname) 创建字符输出流关联本地文件
public FileWriter(File file,boolean append) 创建字符输出流关联本地文件,续写
public FileWriter(String pathname,boolean append) 创建字符输出流关联本地文件,续写

细节1:参数是字符串表示的路径或者File对象都是可以的

细节2:如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的

细节3:如果文件已经存在,则会清空文件,如果不想清空可以打开续写开关

  1. 写数据
成员方法 说明
void write(int c) 写出一个字符
void write(String str) 写出一个字符串
void write(String str,int off,int len) 写出一个字符串的一部分
void write(char[] cbuf) 写出一个字符数组
void write(char[] cbuf,int off,int len) 写出字符数组的一部分

细节:如果write方法的参数是整数,但是实际上写道本地文件中的是整数在字符集上对应的字符

  1. 释放资源

细节:每次使用完流之后都要释放资源

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileWriter;
import java.io.IOException;

public class CharStreamDemo3 {
public static void main(String[] args) throws IOException {
// 1. 创建对象
FileWriter fw = new FileWriter("d.txt");

// 2. 写出数据
// fw.write(25105);
fw.write("我是写出的数据");

fw.write("\r\n");

char[] chars = {'h','i','张','三'};
fw.write(chars);

// 3. 释放资源
fw.close();
}
}

字符流原理解析

  1. 创建字符输入流对象

底层:关联文件,并创建缓冲区(长度为8192的字节数组)

  1. 读取数据

底层:1. 判断缓冲区中是否有数据可以读取

​ 2. 缓冲区没有数据:就从文件中获取数据,装到缓冲区中,每次尽可能装满缓冲区

           3. 缓冲区有数据:就从缓冲区中读取

空参read方法:一次读取一个字节,遇到中文一次读多个字节,并把字节解码转成十进制返回

有参read方法:把读取字节,解码,强转三步合并了,强转之后的字符放到数组中

flush和close

成员方法 说明
public void flush() 将缓冲区中的数据,刷新到本地文件中
public void close() 释放资源/关流

flush:刷新之后,还可以继续往文件中写出数据

close:断开通道,无法再往文件中写出数据

字节流和字符流的使用场景

字节流:

拷贝任意类型的文件

字符流:

读取纯文本文件中的数据

往纯文本文件中写出数据

缓冲流

  • BufferedInputStream

  • BufferedOutputStream

  • BufferedReader

  • BufferedWriter

字节缓冲流

原理:底层自带了长度为8192缓冲区提高性能

方法名称 说明
public BufferedInputStream(InputStream is) 把基本流包装成高级流,提高读取数据的性能
public BufferedOutputStream(OutputStream os) 把基本流包装成高级流,提高写出数据的性能

拷贝文件

利用字节缓冲流拷贝文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;

public class BufferedStreamDemo1 {
public static void main(String[] args) throws IOException {
// 利用字节缓冲流拷贝文件
// 一次读写一个字节

// 1. 创建缓冲流的对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.txt"));

// 2. 循环读取并写到目的地
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}

// 3. 释放资源
bos.close();
bis.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;

public class BufferedStreamDemo2 {
public static void main(String[] args) throws IOException {
// 利用字节缓冲流拷贝文件
// 一次读写多个字节

// 1. 创建缓冲流的对象
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.txt"));

// 2. 循环读取并写到目的地
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes,0,len);
}

// 3. 释放资源
bos.close();
bis.close();
}
}

字节缓冲流提高效率的原理

在Java中,缓冲流(Buffered Streams)可以提高I/O操作的效率。这是因为缓冲流在内部维护了一个数据缓冲区,可以减少直接对硬盘或网络的读写次数

当我们进行读取操作时,缓冲流会一次性从硬盘或网络中读取多个数据到缓冲区,然后我们可以从缓冲区中逐个读取这些数据。这样,即使每次只需要读取一个数据,也不需要每次都进行硬盘或网络操作,从而提高了效率。

同样,当我们进行写入操作时,缓冲流会先将数据写入到缓冲区,当缓冲区满了之后,再一次性将缓冲区中的数据写入到硬盘或网络。这样,即使我们每次只写入一个数据,也不需要每次都进行硬盘或网络操作,从而提高了效率。

总的来说,缓冲流通过减少对硬盘或网络的直接操作次数,从而提高了I/O操作的效率。

字符缓冲流

原理:底层自带了长度为8192的缓冲区提高性能

方法名称 说明
public BufferedReader(Reader r) 把基本流包装为高级流
public BufferedWriter(Writer w) 把基本流包装为高级流

字符缓冲流特有方法

字符缓冲输入流特有方法 说明
public String readLine() 读取一行数据,如果没有数据可读了,会返回Null
字符缓冲输出流特有方法 说明
public void newLine() 跨平台换行

输入流示例:

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

public class BufferedStreamDemo3 {
public static void main(String[] args) throws IOException {
// 字符缓冲输入流

// 1. 创建字符缓冲输入流的对象
BufferedReader br = new BufferedReader(new FileReader("test.txt"));

// 2. 读取数据
// 细节:
// readLine方法在读取的时候,一次读一整行,遇到回车换行结束
// 但是并不会把回车换行读到内存当中
// String line = br.readLine();
// System.out.println(line);

// 读取所有文本内容
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}

// 3. 释放资源
br.close();
}
}

输出流示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class BufferedStreamDemo4 {
public static void main(String[] args) throws IOException {
// 字符缓冲输出流

// 1. 创建字符缓冲输入流的对象
BufferedWriter bw = new BufferedWriter(new FileWriter("e.txt"));

// 2. 写出数据
bw.write("这是一段写入缓冲区流的测试文字");
bw.newLine();
bw.write("这也是一段字符写入缓冲流的测试文字");

// 3. 释放资源
bw.close();

}
}

转换流

转换流是字符流和字节流之间的桥梁

作用一:指定字符集读写(淘汰)

作用二:字节流想要使用字符流中的方法

输入流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;
import java.nio.charset.Charset;

public class ConvertStreamDemo1 {
public static void main(String[] args) throws IOException {
// 利用转换流按照指定字符编码读取
// 该方法已经被JDK11淘汰了

/*// 1. 创建对象并指定字符编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("gbkFile.txt"),"GBK");

// 2. 读取数据
int ch;
while ((ch = isr.read()) != -1) {
System.out.print((char) ch);
}

// 3. 释放资源
isr.close();*/

// 替代方案
// 1. 创建对象
FileReader fr = new FileReader("gbkFile.txt", Charset.forName("GBK"));

// 2. 读取数据
char[] chars = new char[1024];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.println(new String(chars,0,len));
}

// 3. 释放资源
fr.close();
}
}

输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;

public class ConvertStreamDemo2 {
public static void main(String[] args) throws IOException {
// 利用转换流按照指定字符编码写出
// 以下方法同样也是被被淘汰了

/*
// 1. 创建转换流对象
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("OutPutGBKFile.txt"),"GBK");

// 2. 写出数据
osw.write("Hello,世界");

// 3. 释放资源
osw.close();*/

// 替代方案
// 1. 创建对象
FileWriter fw = new FileWriter("OutPutGBKFile.txt", Charset.forName("GBK"));

// 2. 写出数据
fw.write("你好,世界");

// 3. 释放资源
fw.close();
}
}

转换文件编码

方法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.*;

public class ConvertStreamDemo3 {
public static void main(String[] args) throws IOException {
// 将本地GBK文件转换为UTF-8

// JDK11之前的方案
// 1. 创建对象
InputStreamReader isr = new InputStreamReader(new FileInputStream("gbkFile.txt"),"GBK");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("OldOutPutUTF.txt"),"UTF-8");

// 2. 循环读取/写出
int b;
while ((b = isr.read()) != -1) {
osw.write(b);
}

// 3. 释放资源
osw.close();
isr.close();


}
}

方法二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.*;
import java.nio.charset.Charset;

public class ConvertStreamDemo4 {
public static void main(String[] args) throws IOException {
// 将本地GBK文件转换为UTF-8

// JDK11之后的方案
// 1. 创建对象
FileReader fr = new FileReader("gbkFile.txt", Charset.forName("GBK"));
FileWriter fw = new FileWriter("NewOutPutGBKFile.txt",Charset.forName("UTF-8"));

// 2. 写出数据
int b;
while ((b = fr.read()) != -1) {
fw.write(b);
}

// 3. 释放资源
fw.close();
fr.close();
}
}

字节流使用字符流方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.*;
import java.nio.charset.Charset;

public class ConvertStreamDemo5 {
public static void main(String[] args) throws IOException {
// 利用字节流读取文件中的数据,每次读取一整行,而且不能出现乱码
// 字节流在读取中文时,会出现乱码,这时需要字符流
// 字节流里面是没有读取一整行的方法的,这时需要缓冲流

// 1. 创建对象
/*FileInputStream fis = new FileInputStream("gbkFile.txt");
InputStreamReader isr = new InputStreamReader(fis,Charset.forName("GBK"));
BufferedReader br = new BufferedReader(isr);*/

BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("gbkFile.txt"),"GBK"));

String s;
while ((s = br.readLine()) != null) {
System.out.println(s);
}

// 3. 释放资源
br.close();
}
}

序列化流/反序列化流

序列化流(ObjectOutputStream):可以把Java中的对象写到本地文件中,所以也叫对象操作输出流

构造方法 说明
public ObjectOutputStream(OutputStream out) 把基本流包装为高级流
成员方法 说明
public final void writeObejct(Object obj) 把对象序列化(写出)到文件中去

细节:使用对象输出流将对象保存到文件时会出现NotSerializableException异常

解决方案:需要让JavaBean类实现Serializable接口

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class ObjectStreamDemo1 {
public static void main(String[] args) throws IOException {
// 需求:利用序列化流/对象操作输出流,把一个对象写到本地文件中

// 1. 创建对象
Student stu = new Student("张三",18,"男");

// 2. 创建序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("zhangsan.txt"));

// 3. 写出数据
oos.writeObject(stu);

// 4. 释放资源
oos.close();
}
}

反序列化流(ObjectInputStream)

可以把序列化到本地文件中的对象,读取到程序中来

构造方法 说明
public ObjectInputStream(InputStream out) 把基本流变成高级流
成员方法 说明
public Object readObject() 序列化到本地文件中的对象,读取到程序中来

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class ObjectStreamDemo2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {

// 1. 创建反序列化流
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("zhangsan.txt"));

// 2. 读取数据
Object o = ois.readObject();
// 要打印对象值而不是地址值,需要在Javabean中重写toString方法
System.out.println(o.toString());

// 3. 释放资源
ois.close();
}
}

版本号

假设最初的Javabean如下:

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

/*
* Serializable接口里面是没有抽象方法的,所以也叫这种接口为标记型接口
* 一旦实现了这个接口,那么就表示当前的Student类可以被序列化
* */
public class Student implements Serializable {
private String name;
private int age;
private String gender;

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}

将对象序列化输出到文本后,又对Javabean进行了修改,修改为以下内容:

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

/*
* Serializable接口里面是没有抽象方法的,所以也叫这种接口为标记型接口
* 一旦实现了这个接口,那么就表示当前的Student类可以被序列化
* */
public class Student implements Serializable {
private String name;
private int age;
private String gender;
private String[] hobby;

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public String[] getHobby() {
return hobby;
}

public void setHobby(String[] hobby) {
this.hobby = hobby;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}

此时,如果将序列化对象进行反序列化操作,就会提示报错,这是由于Javabean内的版本号不一致导致的

解决方法:

在Javabean中定义常量版本号(固定版本号),写法如下:

1
private static final long serialVersionUID = 1L;

细节

  1. 使用序列化流将对象写到文件时,需要让Javabean类实现Serializable接口。否则,会出现NotSerializableException异常
  2. 序列化流写到文件中的数据是不能修改的,一旦修改就无法再次读回来了
  3. 序列化对象后,修改了Javabean类,再次反序列化,会出问题,会抛出InvalidClassException异常
    • 解决方案:给Javabean类添加serialVersionUID(序列号、版本号)
  4. 如果一个对象中的某个成员变量的值不想被序列化,可以给该成员变量加transient关键字修饰,该关键字标记的成员变量不参与序列化过程

多个对象序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import mytest.Student;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class test1 {
public static void main(String[] args) throws IOException {
/*
* 需求:将多个自定义对象序列化到文件中,但是对象的个数不确定
* */

// 1. 创建对象
Student s1 = new Student("张三",18,"苏州");
Student s2 = new Student("李四",19,"南京");
Student s3 = new Student("王五",17,"无锡");

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("objectsFile.txt"));

// 2. 序列化多个对象
oos.writeObject(s1);
oos.writeObject(s2);
oos.writeObject(s3);

// 3. 释放资源
oos.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class test2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1. 创建反序列化流的对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("objectsFile.txt"));

// 2. 读取数据
// 执行一次方法返回一个对象
try {
while (true) {
Object o = ois.readObject();
System.out.println(o.toString());
}
} catch (EOFException e) {
System.out.println("执行完毕");
}

// 3. 释放资源
ois.close();
}
}

打印流

只有输出流,即OutputStreamWriter的子类:

  • PrintStream
  • PrintWriter

特点:

  1. 打印流只操作文件目的地,不操作数据源
  2. 特有的写出方法可以实现,数据原样写出
  3. 特有的写出方法,可以实现自动刷新,自动换行
    • 打印一次数据 = 写出 + 换行 + 刷新

字节打印流

构造方法 说明
public PrintStream(OutputStream/File/String) 关联字节输出流/文件/文件路径
public PrintStream(String fileName,Charset charset) 指定字符编码
public PrintStream(OutputStream out,boolean autoFlush) 自动刷新
public PrintStream(OutputStream out,boolean autoFlush,String encoding) 指定字符编码且自动刷新

字节流底层没有缓冲区,开不开自动刷新都一样

成员方法 说明
public void write(int b) 常规方法:规则跟之前一样,将指定的字节写出
public void println(Xxx xx) 特有方法:打印任意数据,自动刷新,自动换行
public void print(Xxx xx) 特有方法:打印任意数据,不换行
public void printf(String format,Object… args) 特有方法:带有占位符的打印语句,不换行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;

public class PrintStreamDemo1 {
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
// 1. 创建字节打印流对象
PrintStream ps = new PrintStream(new FileOutputStream("PrintStreamFile1.txt"),true,"UTF-8");

// 2. 写出数据
ps.println(97);
ps.print(true);
ps.println();
ps.printf("%s 一段 %s","这是","文字");

// 3. 释放资源
ps.close();
}
}

字符打印流

构造方法 说明说明
public PrintWriter(Writer/File/String) 关联字节输出流/文件/文件路径
public PrintWriter(String fileName,Charset charset) 指定字符编码
public PrintWriter(Write w,boolean autoFlush) 自动刷新
public PrintWriter(OutputStream out,boolean autoFlush,String encoding) 指定字符编码且自动刷新

字符流底层有缓冲区,想要自动刷新需要开启

成员方法 说明
public void write(int b) 常规方法:规则跟之前一样,将指定的字节写出
public void println(Xxx xx) 特有方法:打印任意数据,自动刷新,自动换行
public void print(Xxx xx) 特有方法:打印任意数据,不换行
public void printf(String format,Object… args) 特有方法:带有占位符的打印语句,不换行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintStreamDemo2 {
public static void main(String[] args) throws IOException {
// 1. 创建字符打印流对象
PrintWriter pw = new PrintWriter(new FileWriter("PrintWriterFile.txt"),true);

// 2. 写出数据
pw.println("PrintWriter方法很好用");

// 3. 释放资源
pw.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.PrintStream;

public class PrintStreamDemo3 {
public static void main(String[] args) {
/*
* 打印流的应用场景
* */

// 获取打印流的对象,此打印流在虚拟机启动的时候,由虚拟机创建,默认指向控制台
// 特殊的打印流,系统中的标准输出流,不能关闭,在系统中是唯一的(如果误关,需要重启虚拟机)
PrintStream ps = System.out;

// 调用打印流中的方法println
ps.println("test");
}
}

(解)压缩流

解压缩流

解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中

Java中只能识别zip压缩文件

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

public class ZipStreamDemo1 {
public static void main(String[] args) throws IOException {
// 1. 创建一个File表示要解压的压缩包
File src = new File("E:\\test.zip");

// 2. 创建一个File表示解压的目的地
File dest = new File("C:\\Users\\Zhao\\Desktop");

unzip(src,dest);
}

// 定义一个方法用来解压
public static void unzip(File src,File dest) throws IOException {
// 解压的本质:把压缩包里面的每一个文件或者文件夹读取出来,按照层级拷贝到目的地当中

// 创建一个解压缩流用来读取压缩包中的数据
ZipInputStream zip = new ZipInputStream(new FileInputStream(src));

// 要先获取到压缩包里面的每一个zipEntry对象
/*for (int i = 0; i < 100; i++) {
ZipEntry entry = zip.getNextEntry();
System.out.println(entry); // 会遍历压缩包里的所有文件,遍历完所有后继续遍历会返回null
}*/

ZipEntry entry;
while ((entry = zip.getNextEntry()) != null) {
System.out.println(entry);
if (entry.isDirectory()) {
// 文件夹:需要在目的地dest处创建一个同样的文件夹
File file = new File(dest,entry.toString());
file.mkdirs();
} else {
// 文件:需要读取到压缩包中的文件,并把他存放到目的地dest文件夹中(按照层级目录进行存放)
FileOutputStream fos = new FileOutputStream(new File(dest,entry.toString()));
int b;
while ((b = zip.read()) != -1) {
// 写到目的地
fos.write(b);
}
fos.close();
zip.closeEntry();
}
}

zip.close();
}
}

压缩流

压缩本质:把每一个(文件/文件夹)看成ZipEntry对象放到压缩包中

单文件压缩

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

public class ZipStreamDemo2 {
public static void main(String[] args) throws IOException {
/*
* 压缩流:
* 需求:
* 把E:\\test.txt打包成一个压缩包
*
* 该示例只支持单文件压缩
* */

// 1. 创建File对象表示要压缩的文件
File src = new File("E:\\test.txt");

// 2. 创建File对象表示压缩包的位置
File dest = new File("C:\\Users\\Zhao\\Desktop");

// 3. 调用方法用来压缩
toZip(src,dest);
}
/*
* 作用:
* 参数一:表示要压缩的文件
* 参数二:表示压缩包的位置
* */
public static void toZip(File src,File dest) throws IOException {
// 1. 创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File(dest,"a.zip")));

// 2. 创建ZipEntry对象,表示压缩包里面的每一个文件和文件夹
ZipEntry entry = new ZipEntry("a.txt");

// 3. 把ZipEntry对象放到压缩包当中
zos.putNextEntry(entry);

// 4. 把src中的数据写到压缩包中
FileInputStream fis = new FileInputStream(src);
int b;
while ((b = fis.read()) != -1) {
zos.write(b);
}

// 5. 释放资源
zos.closeEntry();
zos.close();
}
}

文件夹压缩/多文件压缩

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

public class ZipStreamDemo3 {
public static void main(String[] args) throws IOException {
/*
* 多文件压缩
* */

// 1. 创建File对象表示要压缩的文件夹
File src = new File("E:\\ziptest");

// 2. 创建File对象表示压缩包的父级路径
File destParent = src.getParentFile();

// 3. 创建File对象表示压缩包的路径
File dest = new File(destParent,src.getName() + ".zip");

// 4. 创建压缩流关联压缩包
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(dest));

// 5. 获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
toZip(src,zos, src.getName());

// 6. 释放资源
zos.close();
}


/*
* 作用:获取src里面的每一个文件,变成ZipEntry对象,放入到压缩包当中
* 参数一:数据源
* 参数二:压缩流
* 参数三:压缩包内部的路径
* */
public static void toZip(File src,ZipOutputStream zos,String name) throws IOException {
// 1. 进入src文件夹
File[] files = src.listFiles();

// 2. 遍历数组
for (File file : files) {
if (file.isFile()) {
// 3. 判断-文件,变成ZipEntry对象,放入到压缩包当中
ZipEntry entry = new ZipEntry(name + "\\" + file.getName());
zos.putNextEntry(entry);

// 读取文件中的数据,写到压缩包
FileInputStream fis = new FileInputStream(file);
int b;
while ((b = fis.read()) != -1) {
zos.write(b);
}
fis.close();
zos.closeEntry();
} else {
// 4. 判断-文件夹,递归
toZip(file,zos,name + "\\" + file.getName());
}
}
}
}

Commons-io

Commons-io是apache开源基金组织提供的一组有关IO操作的开源工具包

作用:提高IO流的开发效率

Commons有以下工具类:

  • StringUtils 字符串工具类
  • NumberUtils 数字工具类
  • ArrayUtils 数组工具类
  • RandomUtils 随机数工具类
  • DateUtils 日期工具类
  • StopWatch 秒表工具类
  • ClassUtils 反射工具类
  • SystemUtils 系统工具类
  • MapUtils 集合工具类
  • Beanutils bean工具类
  • …等等

Commons-io使用步骤

  1. 在项目中创建一个文件夹:lib
  2. 将jar包复制粘贴到lib文件夹
  3. 右键点击jar包,选择 Add as Library -> 点击OK
  4. 在类中导包使用

FileUtils

FileUtils类(文件/文件夹相关) 说明
static void copyFile(File srcFile,File destFile) 复制文件
static void copyDirectory(File srcDir,File destDir) 复制文件夹
static void copyDirectoryToDirectory(File srcDir,File destDir) 复制文件夹
static void deleteDirectory(File directory) 删除文件夹
static void cleanDirectory(File directory) 清空文件夹
static String readFileToString(File file,Charset encoding) 读取文件中的数据变成字符串
static void write(File file,CharSequence data,String encoding) 写出数据

IOUtils类

IOUtils类 说明
public static int copy(InputStream input,OutputStream output) 复制文件
public static int copyLarge(Reader input,Writer output) 复制大文件
public static String readLines(Reader input) 读取数据
public static void write(String data,OutputStream output) 写出数据

Hutool工具包

API文档:入门和安装 (hutool.cn)

相关类 说明
IoUtil 流操作工具类
FileUtil 文件读写和操作的工具类
FileTypeUtil 文件类型判断工具类
WatchMonitor 目录、文件监听
ClassPathResource 针对ClassPath中资源的访问封装
FileReader 封装文件读取
FileWriter 封装文件写入

多线程

线程:

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

进程:

进程是程序的基本执行实体。

多线程的应用场景:

  • 软件中的耗时操作:拷贝、迁移大文件,加载大量的资源文件
  • 所有的聊天软甲
  • 所有的后台服务器

并发和并行

并发:在同一时刻,有多个指令在单个CPU上交替执行

并行:在同一时刻,有多个指令在多个CPU上同时执行

多线程的实现方式

  1. 继承Thread类的方式进行实现
  2. 实现Runnable接口的方式进行实现
  3. 利用Callable接口和Future接口方式实现

Thread类

自定义类:

1
2
3
4
5
6
7
8
9
public class MyThread extends Thread{
@Override
public void run() {
// 1. 书写线程要执行代码
for (int i = 0; i < 100; i++) {
System.out.println(getName() + ":Hello World!");
}
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadDemo1 {
public static void main(String[] args) {
/*
* 多线程的第一种启动方式:
* 1. 自己定义一个类继承Thread
* 2. 重写run方法
* 3. 创建子类对象,并启动线程
* */

MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

t1.setName("线程1");
t2.setName("线程2");

// 1. 开启线程
t1.start();
t2.start();
}
}

Runnable接口

自定义类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package threadcase2;

public class MyRun implements Runnable{

@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 获取到当前线程的对象
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ":Hello World!");
}
}
}

测试类:

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

public class ThreadDemo1 {
public static void main(String[] args) {
/*
* 多线程的第二种启动方式:
* 1. 自己定义一个类实现Runnable接口
* 2. 重写里面的run方法
* 3. 创建自己的类的对象
* 4. 创建一个Thread类的对象,并开启线程
* */

// 1. 创建自己的类的对象
// 表示多线程要执行的任务
MyRun mr = new MyRun();

// 创建线程对象
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);

// 给线程设置名字
t1.setName("线程1:");
t2.setName("线程2:");

// 开启线程
t1.start();
t2.start();
}
}

Callable接口和Future接口

自定义类:

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

public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 求 1~100之间的和
int sum = 0;

for (int i = 0; i <= 100; i++) {
sum += i;
}

return sum;
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*
* 多线程的第三种实现方式:
* 特点:可以获取到多线程运行的结果
*
* 1. 创建一个类MyCallable实现Callable接口
* 2. 重写call方法(是有返回值的,表示多线程运行的结果)
* 3. 创建MyCallable的对象(表示多线程要执行的任务)
* 4. 创建FutureTask的对象(作用:管理多线程运行的结果)
* 5. 创建Thread类的对象,并启动(表示线程)
* */

// 创建自己的类的对象
MyCallable mc = new MyCallable();

// 创建FutureTask的对象
FutureTask<Integer> ft = new FutureTask<>(mc);

// 创建线程的对象
Thread t1 = new Thread(ft);

// 启动线程
t1.start();

// 获取多线程的结果
Integer res = ft.get();

System.out.println(res);
}
}

选择方式

方式 优先 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 可拓展性较差,不能再继承其他的类
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用Thread类中的方法
实现Callable接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能直接使用Thread类中的方法

Thread常见成员方法

方法名称 说明
String getName() 返回此线程的名称
void setName(String name) 设置线程的名称
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位毫秒
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
final void setDaemon(boolean on) 设置为守护线程
public static void yield() 出让线程/礼让线程
public static void join() 插入线程/插队线程

方法setName(String name)细节:

  1. 如果没有个线程设置名字,线程也是会有默认的名字的。
    • 格式:Thread-X(X表示序号,从0开始)
  2. 构造方法中可以设置名称

方法static Thread currentThread()细节:

JVM虚拟机启动之后,会自动的启动多条线程

其中有一条线程就叫做main线程

它的作用就是去调用main方法,并执行里面的代码

方法static void sleep(long time)细节:

  1. 哪条线程执行到这个方法,哪条线程就会在这个地方停留相应的时间
  2. 方法的参数:表示睡眠的时间,单位毫秒
  3. 当时间到了之后,线程会自动醒来,继续执行下面的其他代码

方法setDaemon(boolean on)细节:

当其他的非守护线程执行完毕之后,守护线程会陆续结束

线程优先级

抢占式调度:随机的选择线程执行

非抢占式调度:按照顺序执行线程

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

public class ThreadDemo {
public static void main(String[] args) {
/*
* setPriority(int newPriority) 设置线程优先级
* final int getPriority() 获取线程优先级
* */

// 创建线程要执行的参数对象
MyRunnable mr = new MyRunnable();

// 创建线程对象
Thread t1 = new Thread(mr,"Thread01");
Thread t2 = new Thread(mr,"Thread02");

System.out.println(t1.getPriority()); // 不设置的情况下,优先级默认为5
System.out.println(t2.getPriority()); // 不设置的情况下,优先级默认为5

t1.setPriority(1);
t2.setPriority(10);

t1.start();
t2.start();

/*
虽然t2优先级比t1高,但是不代表执行的时候会先完全执行完t2再执行t1
只不过t2优先执行的概率更高
*/
}
}

线程的生命周期

flowchart LR
    START["创建线程对象"] -- "start()" --> THEN1["有执行资格
没有执行权"] THEN1 -- 抢到CPU的执行权 --> THEN2["有执行资格
有执行权"] THEN2 -- 其他线程抢走CPU的执行权 --> THEN1 THEN2 --> THEN3["没有执行资格
没有执行权"] THEN3 -- sleep或者其他阻塞方法 --> THEN1 THEN2 --> END["线程死亡
变成垃圾"]

线程安全问题

在 Java 中,线程安全是指在多线程环境中,多个线程同时访问共享资源时,不会导致数据不一致或其他意外情况。线程安全的主要目标是确保数据的一致性和正确性。

示例:

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

public class MyThread extends Thread{
static int ticket = 0;

@Override
public void run() {
while (true) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}

问题分析

  1. 数据竞争:多个线程同时读取和修改 ticket 变量,可能会导致多个线程同时增加 ticket,从而导致票数不正确。

同步代码块

把操作共享数据的代码锁起来

格式:

1
2
3
synchronized(锁){
操作共享数据的代码
}

特点1:锁默认打开,有一个线程进去了,锁自动关闭

特点2:里面的代码全部执行完毕后,线程出来,锁自动打开

示例:

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

public class MyThread extends Thread{
static int ticket = 0;

// 锁对象,一定要是唯一的
static Object obj = new Object();

@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
}
}
}
}

通常来说,锁对象一般为当前类的字节码对象,如以上示例,锁对象可以为MyThread.class

同步方法

就是把synchronized关键字加到方法上

格式:

1
修饰符	synchronized 返回值类型 方法名(方法参数) {...}

特点1:同步方法是锁住方法里面所有的代码

特点2:锁对象不能自己指定

若为非静态方法:this

若为静态方法:当前类的字节码文件对象

Lock锁

由于synchronized并不能清晰的表达如何加锁和释放锁,所以JDK5以后提供了一个新的锁的对象

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

Lock中提供了获得锁的释放锁的方法

void lock():获得锁

void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

ReentrantLock的构造方法

ReentrantLock():创建一个ReentranLock的实例

实现案例:

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

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyThread extends Thread {
static int ticket = 0;

// 加上static,是防止该类的实现方法创建多个锁
static Lock lock = new ReentrantLock();

@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket < 100) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
ticket++;
System.out.println(getName() + "正在卖第" + ticket + "张票");
} else {
break;
}
} catch (RuntimeException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}

这里使用try...catch...finally的格式,是需要保证每个线程可以将锁释放,否则会导致程序无法自动结束

死锁

简单理解:死锁就是两个锁嵌套,需要避免这种错误

生产者和消费者(等待唤醒机制)

生产者消费者模式是一个十分经典的多线程协作的模式

生产者:生产数据

消费者:消费数据

常见方法

方法名称 说明
void wait() 当前线程等待,直到被其他线程唤醒
void notify() 随机唤醒单个线程
void notifyAll() 唤醒所有线程

示例:

生产者代码:

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

public class Creater extends Thread{
@Override
public void run() {
/*
生产者
*/

/*
1. 循环
2. 同步代码块(后期可以改同步方法或lock锁)
3. 判断共享数据是否到了末尾(是)
4. 判断共享数据是否到了末尾(否)
*/

while (true) {
synchronized (Middle.lock) {
if (Middle.count == 0) {
break;
} else {
// 判断中间体状态
if (Middle.flag == 1) {
// 如果为1,表示停止生产,等待消费者消费
try {
Middle.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// 如果为0,表示需要生产者生产
System.out.println("生产者生产了一个物品");

// 修改中间体状态
Middle.flag = 1;

// 唤醒消费者
Middle.lock.notifyAll();
}
}
}
}
}
}

消费者代码:

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

public class User extends Thread{
@Override
public void run() {
/*
消费者
*/

/*
1. 循环
2. 同步代码块(后期可以改同步方法或lock锁)
3. 判断共享数据是否到了末尾(是)
4. 判断共享数据是否到了末尾(否)
*/

while (true) {
synchronized (Middle.lock){
if (Middle.count == 0) {
break;
} else {
// 先判断中间体是否存放了生产者的生成物
if (Middle.flag == 0) {
// 如果没有,就等待
try {
Middle.lock.wait();// 让当前线程跟锁进行绑定
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
// 把消费的次数-1
Middle.count--;

// 如果有,就由消费者消费
System.out.println("消费者在消费,还能消费" + Middle.count + "次");

// 消费完后,唤醒生产者继续生产
Middle.lock.notifyAll();

// 修改中间体状态
Middle.flag = 0;
}
}
}
}
}
}

中间体

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

public class Middle extends Thread{
/*
作用:控制生产者和消费者的执行
* */

// 生产者是否产出,有:1,没有:0
public static int flag=0;

// 总个数
public static int count = 10;

// 锁对象
public static Object lock = new Object();


}

测试类:

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

public class ThreadDemo {
public static void main(String[] args) {
/*
需求:完成生产者和消费者(等待唤醒机制)的代码
实现线程轮流交替执行的效果
*/

// 创建线程对象
Creater c = new Creater();
User u = new User();

// 给线程设置名称
c.setName("生产者");
u.setName("消费者");

// 开启线程
c.start();
u.start();
}
}

阻塞队列方式

阻塞队列的继承结构

接口:IterableCollectionQueueBlockingQueue

实现类:ArrayBlockQueueLinkedBlockingQueue

生产者:

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

import java.util.concurrent.ArrayBlockingQueue;

public class Creater extends Thread{

ArrayBlockingQueue<String> queue;

public Creater(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断的将产物放到阻塞队列当中
try {
queue.put("产物"); // 底层已经创建了锁对象,不需要写同步代码块
System.out.println("生产者生产了一个产物");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

消费者:

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

import java.util.concurrent.ArrayBlockingQueue;

public class User extends Thread{
ArrayBlockingQueue<String> queue;

public User(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}

@Override
public void run() {
while (true) {
// 不断的将产物放到阻塞队列当中
try {
String take = queue.take(); // 底层已经创建了锁对象,不需要写同步代码块
System.out.println(take);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

测试类:

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

import java.util.concurrent.ArrayBlockingQueue;

public class ThreadDemo {
public static void main(String[] args) {
/*
需求:利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码
细节:生产者和消费者必须使用同一个阻塞队列
*/

// 1. 创建阻塞队列的对象
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

// 2. 创建线程对象,并把阻塞队列传递到对象中去
Creater c = new Creater(queue);
User u = new User(queue);

c.setName("生产者");
u.setName("消费者");

c.start();
u.start();

}
}

线程的状态

线程可以处于下列状态之一:

  • NEW(新建状态):至今尚未启动的线程处于这种状态
  • RUNNABLE(就绪状态):正在Java虚拟机中执行的线程处于这种状态
  • BLOCKED(阻塞状态):受阻塞并等待某个监视器锁的线程处于这种状态
  • WAITING(等待状态):无限期地等待另一个线程来执行某一特定操作的线程处于这种状态
  • TIMED_WAITING(计时状态):等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态
  • TERMINATED(结束状态):已退出的线程处于这种状态

在给定时间点上,一个线程只能处于一种状态。这些状态是虚拟机状态,它们并没有反映所有操作系统线程状态。

线程池

以前的多线程弊端

1
2
3
4
5
6
class MyThread extends Thread{
@override
public void run() {
多线程执行的一些代码
}
}
1
2
3
4
5
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();

t1.start();
t2.start();

弊端一:用到线程的时候就创建

弊端二:用完之后线程就消失

核心原理

  1. 创建一个池子,池子中是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完成,线程归还给池子。下回再次提交任务时,不需要创建任务时,不需要创建新的线程,直接复用已有的线程即可
  3. 如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

代码实现

  1. 创建线程池

  2. 提交任务

  3. 所有的任务全部执行完毕,关闭线程池

创建线程池

Executors:线程池工具通过调用方法返回不同类型的线程池对象

方法名称 说明
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池

方法一:

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

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
/*
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
*/

// 1. 获取线程池
ExecutorService pool1 = Executors.newCachedThreadPool();

// 2. 提交任务(线程复用)
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());
Thread.sleep(1000);
pool1.submit(new MyRunnable());

// 3. 销毁线程池
// pool1.shutdown();
}
}

方法二:

1
2
3
4
5
6
7
8
9
10
package Y0threadpool1;

public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + "---" + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package Y0threadpool1;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
/*
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
*/

// 1. 获取线程池
ExecutorService pool1 = Executors.newFixedThreadPool(3);

// 2. 提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable()); // 最多只会出现pool-1-thread-3

// 3. 销毁线程池
// pool1.shutdown();
}
}

自定义线程池

核心元素一:核心线程数量

核心元素二:线程池中最大线程的数量

核心元素三:空闲时间(值)

核心元素四:空闲时间(单位)

核心元素五:阻塞队列

核心元素六:创建线程的方式

核心元素七:要执行的任务过多时的解决方案

注意点:

  1. 当核心线程满时,再提交任务就会排队
  2. 当核心线程满时,队伍也满时,会创建临时线程
  3. 当核心线程、队伍、临时线程都满时,会触发任务拒绝策略

任务拒绝策略

任务拒绝策略 说明
ThreadPoolExecutor.AbortPolicy 默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常(不推荐)
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行

线程池的大小

CPU密集型计算:最大并行数+1

假设CPU是4核8线程,那么计算过程就是:8 + 1

I/O密集型运算:最大并行数 * 期望CPU利用率 * (总时间(CPU计算时间 + 等待时间) / CPU计算时间)

假设CPU是4核8线程,希望CPU利用率为100%,且需要进行以下操作:

  1. 读取两个数据(耗时:1s)
  2. 相加(耗时:1s)

那么计算过程就是

8 * 100% * ((50% + 50%) / 50%) = 16

并行数

查看Java虚拟机可以的处理器的数目

1
2
int count = Runtime.getRuntime().availableProcessors();
System.out.println(count)

网络编程

Java中可以使用Java.net包下的技术轻松开发出常见的网络应用程序

常见的软件架构

C/S——Client/Server(客户端/服务器):

在用户本地需要下载并安装客户端程序, 在远程有一个服务器端程序

B/S——Browser/Server(浏览器/服务器):

只需要一个浏览器,用户通过不同的网址。客户访问不同的服务器

B/S架构的优缺点

  1. 不需要开发客户端,只需要页面+服务端
  2. 用户不需要下载,打开浏览器就能使用
  3. 如果应用过大,用户体验受到影响

C/S架构的优缺点

  1. 画面可以做的非常精美,用户体验好
  2. 需要开发客户端
  3. 用户需要下载和更新的时候太麻烦

网络编程三要素

IP + 端口号 + 协议

IP:设备在网络中的地址,是唯一的标识

端口号:应用程序在设备中唯一的标识

协议:数据在网络传输中的规则,常见的协议有:UDP、TCP、HTTP、HTTPS、FTP

InetAddress

方法名 说明
static InetAddress getByName(String host) 确定主机名称的IP地址。主机名称可以是机器名称,也可以是IP地址
String getHostName() 获取此IP地址的主机名
String getHostAddress() 返回文本显示中的IP地址字符串

端口号

端口号范围:0~65535

其中,0~1023之间的端口号预留给系统,1024以上的端口可以提供开发

注意:一个端口只能由一个应用占用

协议

TCP/IP:

  1. 应用层
  2. 表示层
  3. 会话层
  4. 传输层
  5. 网络层
  6. 数据链路层
  7. 物理层

应用层:HTTP、FTP、Telnet、DNS

传输层:TCP、UDP

网络层:IP、ICMP、ARP

UDP协议

  • 用户数据包协议(User Datagram Protocol)
  • UDP是面向无连接通信协议。速度快,有大小限制一次最多发送64K,数据不安全,易丢失数据。

TCP协议

  • 传输控制协议TCP(Transmission Control Protocol)
  • TCP协议是面向连接的通信协议。速度慢,没有大小限制,数据安全。

UDP通信程序

发送数据

  1. 创建发送端的DatagramSocket对象
  2. 数据打包(DatagramPacket)
  3. 发送数据
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package udpdemo;

import java.io.IOException;
import java.net.*;

public class SendMessageDemo {
public static void main(String[] args) throws IOException {
// 1. 发送数据
// 创建DatagramSocket对象
// 细节:
// 空参:所有可用的端口中随机一个进行使用
// 有参:指定端口进行绑定
DatagramSocket ds = new DatagramSocket();

// 2. 打包数据
String str = "你好啊!!!";
byte[] bytes = str.getBytes();
InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,port);

// 3. 发送数据
ds.send(dp);

// 4. 释放资源
ds.close();
}
}

接收数据

  1. 创建接收端的DatagramSocket对象
  2. 接收打包好的数据
  3. 解析数据包
  4. 释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package udpdemo;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class ReceiveMessageDemo {
public static void main(String[] args) throws IOException {
// 1. 接收数据
// 创建DatagramSocket对象
// 细节:
// 在接收的时候,一定要绑定端口
// 并且绑定的端口一定要和发送的端口保持一致
DatagramSocket ds = new DatagramSocket(10086);

// 2. 接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
// 该方法是阻塞的
// 程序执行到这一步的时候,会在这里等待
// 等发送端发送消息
ds.receive(dp);

// 3. 解析数据包
byte[] data = dp.getData();
int length = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();

System.out.println("接收到的数据为:" + new String(data,0, length));
System.out.println("该数据是从" + address + "这台电脑的" + port + "端口发出的");

// 释放资源
ds.close();
}
}

练习

按照下面的要求实现程序

UDP发送数据:数据来自于键盘录入,直到输入的数据为886,发送数据结束

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

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class SendMessageDemo {
public static void main(String[] args) throws IOException {
/*
按照下面的要求实现程序
UDP发送数据:数据来自于键盘录入,直到输入的数据为886,发送数据结束
UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
*/

// 1. 创建对象
DatagramSocket ds = new DatagramSocket();

// 2. 打包数据
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请输入:");
String str = sc.nextLine();
if ("886".equals(str)) break;
byte[] bytes = str.getBytes();

InetAddress address = InetAddress.getByName("127.0.0.1");
int port = 10086;
DatagramPacket dp = new DatagramPacket(bytes, bytes.length,address,port);

// 3. 发送数据
ds.send(dp);
}

// 4. 释放资源
ds.close();
}
}

UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收

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

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class ReceiveMessageDemo {
public static void main(String[] args) throws IOException {
// 1. 接收数据
// 创建DatagramSocket对象
DatagramSocket ds = new DatagramSocket(10086);

// 2. 接收数据包
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
while (true) {
ds.receive(dp);

// 3. 解析数据包
byte[] data = dp.getData();
int length = dp.getLength();
InetAddress address = dp.getAddress();
int port = dp.getPort();

System.out.println("接收到的数据为:" + new String(data,0, length));
System.out.println("该数据是从" + address + "这台电脑的" + port + "端口发出的");
}
}
}

UDP的三种通信方式

  1. 单播:之前的练习就是单播

  2. 组播:

    1. 组播地址:224.0.0.0 ~ 239.255.255.255
    2. 其中:224.0.0.0 ~ 224.0.0.255 为预留的组播地址

    发送端:

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

    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.MulticastSocket;
    import java.util.Scanner;

    public class SendMessageDemo {
    public static void main(String[] args) throws IOException {
    /*
    按照下面的要求实现程序
    UDP发送数据:数据来自于键盘录入,直到输入的数据为886,发送数据结束
    UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
    */

    // 1. 创建对象
    MulticastSocket ms = new MulticastSocket();

    // 2. 打包数据
    Scanner sc = new Scanner(System.in);
    while (true) {
    System.out.println("请输入:");
    String str = sc.nextLine();
    if ("886".equals(str)) break;
    byte[] bytes = str.getBytes();

    InetAddress address = InetAddress.getByName("224.0.0.1");
    int port = 10000;
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length,address,port);

    // 3. 发送数据
    ms.send(dp);
    }

    // 4. 释放资源
    ms.close();
    }
    }

    接收端:

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

    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.MulticastSocket;

    public class ReceiveMessageDemo1 {
    public static void main(String[] args) throws IOException {
    // 1. 接收数据
    // 创建MuticastSocket对象
    MulticastSocket ms = new MulticastSocket(10000);

    // 将当前本机,添加到224.0.0.1的这一组当中
    InetAddress address = InetAddress.getByName("224.0.0.1");
    ms.joinGroup(address);

    // 2. 接收数据包
    byte[] bytes = new byte[1024];
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

    ms.receive(dp);

    // 3. 解析数据
    byte[] data = dp.getData();
    int len = dp.getLength();
    String ip = dp.getAddress().getHostAddress();
    String name = dp.getAddress().getHostName();

    System.out.println("ip为:" + ip +"主机名为:" + name + "的人,发送了数据" + new String(data,0,len));

    // 释放资源
    ms.close();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    package udpdemo3;

    import java.io.IOException;
    import java.net.DatagramPacket;
    import java.net.InetAddress;
    import java.net.MulticastSocket;

    public class ReceiveMessageDemo3 {
    public static void main(String[] args) throws IOException {
    // 1. 接收数据
    // 创建MuticastSocket对象
    MulticastSocket ms = new MulticastSocket(10000);

    // 将当前本机,添加到224.0.0.1的这一组当中
    InetAddress address = InetAddress.getByName("224.0.0.1");
    ms.joinGroup(address);

    // 2. 接收数据包
    byte[] bytes = new byte[1024];
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

    ms.receive(dp);

    // 3. 解析数据
    byte[] data = dp.getData();
    int len = dp.getLength();
    String ip = dp.getAddress().getHostAddress();
    String name = dp.getAddress().getHostName();

    System.out.println("ip为:" + ip +"主机名为:" + name + "的人,发送了数据" + new String(data,0,len));

    // 释放资源
    ms.close();
    }
    }
  3. 广播:

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

    import java.io.IOException;
    import java.net.*;
    import java.util.Scanner;

    public class SendMessageDemo {
    public static void main(String[] args) throws IOException {
    /*
    按照下面的要求实现程序
    UDP发送数据:数据来自于键盘录入,直到输入的数据为886,发送数据结束
    UDP接收数据:因为接收端不知道发送端什么时候停止发送,故采用死循环接收
    */

    // 1. 创建对象
    DatagramSocket ds = new DatagramSocket();

    // 2. 打包数据
    Scanner sc = new Scanner(System.in);
    while (true) {
    System.out.println("请输入:");
    String str = sc.nextLine();
    if ("886".equals(str)) break;
    byte[] bytes = str.getBytes();

    InetAddress address = InetAddress.getByName("255.255.255.255");
    int port = 10086;
    DatagramPacket dp = new DatagramPacket(bytes, bytes.length,address,port);

    // 3. 发送数据
    ds.send(dp);
    }

    // 4. 释放资源
    ds.close();
    }
    }

TCP通信程序

TCP通信协议是一种可靠的网络协议,它在通信的两端各建立一个Socket对象

通信之前要保证连接已经建立

通过Socket产生IO流来进行网络通信

客户端:

  1. 创建客户端的Socket对象(Socket)与指定服务端连接
    1. Socket(String host, int port)
  2. 获取输出流,写数据
    1. OutputStream getOutputStream()
  3. 释放资源
    1. void close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package tcpdemo1;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class Client {
public static void main(String[] args) throws IOException {
// TCP协议,发送数据

// 1. 创建Socket对象
// 细节:在创建对象的同时会连接服务端
// 如果连接不上,代码会报错
Socket socket = new Socket("127.0.0.1",10000);

// 2. 可以从连接通道中获取输出流
OutputStream os = socket.getOutputStream();
// 写出数据
os.write("你好".getBytes());

// 3. 释放资源
os.close();
socket.close();
}
}

服务器:

  1. 创建服务器端的Socket对象(ServerSocket)

    1. ServerSocket(int port)
  2. 监听客户端连接,返回一个Socket对象

    1. Socket accept()
  3. 获取输入流,读数据,并把数据显示在控制台

    1. InputStream getInputStream()
  4. 释放资源

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

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
public static void main(String[] args) throws IOException {
// TCP协议,接收数据

// 1. 创建对象ServerSocket
ServerSocket ss = new ServerSocket(10000);

// 2. 监听客户端的连接
Socket socket = ss.accept();

// 3. 从连接通道中获取输入流读取数据
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
int b;
while ((b = isr.read()) != -1) {
System.out.println((char)b);
}

// 4. 释放资源
socket.close();
ss.close();
}
}

多发多收

客户端:

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

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
public static void main(String[] args) {
// 需求:
// 客户端多次发送数据
// 服务器多次接收数据

while (true) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream();
Scanner sc = new Scanner(System.in);

while (true) {
System.out.println("请输入:");
String str = sc.nextLine();
os.write((str + "\n").getBytes());
os.flush(); // 确保数据立即发送

if ("关闭".equals(str)) {
break;
}
}

os.close();
socket.close();
break; // 退出外层循环
} catch (IOException e) {
System.out.println("连接失败,重新连接中...");
try {
Thread.sleep(2000); // 等待2秒后重试
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
}
}

服务端:

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

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(9999);
while (true) {
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);

int b;
StringBuilder sb = new StringBuilder();
while ((b = isr.read()) != -1) {
System.out.print((char)b);
sb = sb.append((char)b);
}
System.out.println("");
if (sb.toString().equals("关闭")) {
socket.close();
ss.close();
break;
} else {
socket.close();
}
}

}
}

接收和反馈

客户端:

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

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class Client {
public static void main(String[] args) {
// 需求:
// 客户端发送数据,并将服务端的响应数据打印出来
// 服务端接收数据,并给客户端发送响应数据

while (true) {
try {
Socket socket = new Socket("127.0.0.1", 10001);
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
Scanner sc = new Scanner(System.in);

while (true) {
System.out.println("请输入:");
String str = sc.nextLine();
os.write((str + "\n").getBytes());
os.flush(); // 确保数据立即发送

int b;
StringBuilder sb = new StringBuilder();
while ((b = isr.read()) != -1) {
char c = (char) b;
if (c == '\n') break; // 读取到换行符时结束
sb.append(c);
}
System.out.println("服务器响应:" + sb.toString());

if ("关闭".equals(str)) {
break;
}
}

os.close();
socket.close();
break; // 退出外层循环
} catch (IOException e) {
System.out.println("连接失败,重新连接中...");
try {
Thread.sleep(2000); // 等待2秒后重试
} catch (InterruptedException ie) {
ie.printStackTrace();
}
}
}
}
}

服务端:

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

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(10001);
while (true) {
Socket socket = ss.accept();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
OutputStream os = socket.getOutputStream();

while (true) {
// 解析数据
int b;
StringBuilder sb = new StringBuilder();
while ((b = isr.read()) != -1) {
char c = (char) b;
System.out.print(c);
sb.append(c);
if (c == '\n') break; // 读取到换行符时结束
}
System.out.println("");

// 响应数据
os.write("收到\n".getBytes());
os.flush();

// 关闭连接
if (sb.toString().trim().equals("关闭")) {
socket.close();
ss.close();
break;
}
}
}
}
}

上传文件

客户端:

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

import java.io.*;
import java.net.*;

public class Server {
public static void main(String[] args) {
try (ServerSocket ss = new ServerSocket(10002)) {
Socket socket = ss.accept();
try (InputStream fis = socket.getInputStream();
OutputStream os = socket.getOutputStream();
FileOutputStream fos = new FileOutputStream("A_test.txt")) {
int b;
while ((b = fis.read()) != -1) {
char c = (char) b;
fos.write(b);
if (c == '\n') {
os.write("接收完毕".getBytes());
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

服务端:

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

import java.io.*;
import java.net.*;

public class Client {
public static void main(String[] args) {
try (Socket socket = new Socket("127.0.0.1", 10002);
FileInputStream fis = new FileInputStream("test.txt");
OutputStream os = socket.getOutputStream();
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is)) {

byte[] bytes = new byte[1024 * 1024 * 5];
int len;
while ((len = fis.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
os.write("\n".getBytes());
os.flush(); // 确保数据被发送

int b;
while ((b = isr.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

反射

反射允许对封装类的字段,方法和构造函数的信息进行编程访问

获取Class对象

共有三种方法:

  1. Class.forName(“全类名”);
  2. 类名.class
  3. 对象.getClass();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 第一种方式
// Class.forName("全类名")
// 全类名:包名+类名
// 最常用
Class clazz1 = Class.forName("MyReflectDemo1.Student");

// 2. 第二种方式
// 一般当作参数进行传递,如多线程的锁
Class clazz2 = Student.class;

// 3. 第三种方式
// 当有类的对象时才可以使用
Student s = new Student();
Class clazz3 = s.getClass();
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true

获取构造方法

Class类中用于获取构造方法的方法

方法 说明
Constructor<?>[] getConstructors() 返回所有公共构造方法对象的数组
Constructor<?>[] getDeclaredConstructors() 返回所有构造方法对象的数组
Constructor<T> getConstructor(Class<?>… parameterTypes) 返回单个公共构造方法对象
Constructor<T> getDeclaredConstructor(Class<?>… parameterTypes) 返回单个构造方法对象

Constructor类中用于创建对象的方法

方法 说明
T newInstance(Object… initargs) 根据指定的构造方法创建对象
setAccessible(boolean flag) 设置为true,表示取消访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package MyReflectDemo2;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;

public class myreflectdemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 1. 获取class字节码文件对象
Class clazz = Class.forName("MyReflectDemo2.Student");

// 2. 获取构造方法
Constructor[] cons1 = clazz.getConstructors();

for (Constructor con : cons1) {
// public MyReflectDemo2.Student()
// public MyReflectDemo2.Student(java.lang.String,int)
System.out.println(con);
}

Constructor[] cons2 = clazz.getDeclaredConstructors();
for (Constructor con : cons2) {
// public MyReflectDemo2.Student()
// public MyReflectDemo2.Student(java.lang.String,int)
// protected MyReflectDemo2.Student(java.lang.String)
// private MyReflectDemo2.Student(int)
System.out.println(con);
}

Constructor decon1 = clazz.getDeclaredConstructor();
// public MyReflectDemo2.Student()
System.out.println(decon1);

Constructor decon2 = clazz.getDeclaredConstructor(String.class);
// protected MyReflectDemo2.Student(java.lang.String)
System.out.println(decon2);

Constructor decon3 = clazz.getDeclaredConstructor(String.class,int.class);
// public MyReflectDemo2.Student(java.lang.String,int)
System.out.println(decon3);

// 获取权限修饰符
int modifiers = decon3.getModifiers(); // public == 1 | protected == 4 | private == 2
System.out.println(modifiers);

// 获取参数
Parameter[] parameters = decon3.getParameters();
for (Parameter parameter : parameters) {
// java.lang.String arg0
// int arg1
System.out.println(parameter);
}

// 创建对象
Student stu = (Student) decon3.newInstance("张三", 23);
// Student{name='张三', age=23}
System.out.println(stu);

// 暴力反射:表示临时取消权限校验
Constructor decon4 = clazz.getDeclaredConstructor(int.class);
decon4.setAccessible(true);
Student student = (Student) decon4.newInstance(13);
// Student{name='null', age=13}
System.out.println(student);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package MyReflectDemo2;

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

public Student(){};

private Student(int age) {
this.age = age;
}

protected Student(String name) {
this.name = name;
}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

获取字段(成员变量)

Class类中用于获取成员变量的方法

方法 说明
Field[] getFields() 返回所有公共成员变量对象的数组
Field[] getDeclaredFields() 返回所有成员变量对象的数组
Field getField(String name) 返回单个公共成员变量对象
Field getDeclaredField(String name) 返回单个成员变量对象

Field类中用于创建对象的方法

方法 说明
void set(Object obj,Object value) 赋值
Object get(Object obj) 获取值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package MyReflectDemo3;

import MyReflectDemo3.Student;

import java.lang.reflect.Field;

public class myreflectdemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
// 1. 获取class字节码文件的对象
Class clazz = Class.forName("MyReflectDemo3.Student");

// 2. 获取成员变量
Field[] fields1 = clazz.getFields();
for (Field field : fields1) {
// public java.lang.String MyReflectDemo3.Student.gender
System.out.println(field);
}

Field[] fields2 = clazz.getDeclaredFields();
for (Field field : fields2) {
// private java.lang.String MyReflectDemo3.Student.name
// private int MyReflectDemo3.Student.age
// public java.lang.String MyReflectDemo3.Student.gender
System.out.println(field);
}

// 3. 获取单个成员变量
Field gender = clazz.getField("gender");
// public java.lang.String MyReflectDemo3.Student.gender
System.out.println(gender);

Field name = clazz.getDeclaredField("name");
// private java.lang.String MyReflectDemo3.Student.name
System.out.println(name);

// 获取权限修饰符
int modifiers = name.getModifiers();
System.out.println(modifiers);

// 获取数据类型
Class type = name.getType();
System.out.println(type); // class java.lang.String

// 获取成员变量记录的值
Student s = new Student("张三",18,"男");
name.setAccessible(true);
System.out.println(name.get(s)); // 张三

// 修改成员变量记录的值
name.set(s,"李四");
// Student{name='李四', age=18}
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package MyReflectDemo3;

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

public String gender;

public Student() {}

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

获取成员方法

Class类中用于获取成员方法的方法

方法 说明
Method[] getMethods() 返回所有公共成员方法对象的数组,包括继承的
Method[] getDeclaredMethods() 返回所有成员方法对象的数组,包括继承的
Method getMethod(String name,Class<?>… parameterTypes) 返回单个公共成员方法对象
Method getDeclaredMethod(String name,Class<?>… parameterTypes) 返回单个公共成员方法对象

Method类中用于创建对象的方法

方法 说明
Object invoke(Object obj,Object… args) 运行方法
参数一:用obj对象调用该方法 参数二:调用方法的传递的参数(没有就不写) 返回值:方法的返回值(没有就不写)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package MyReflectDemo4;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class myflectdemo {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 1. 获取
Class clazz = Class.forName("MyReflectDemo4.Student");

// 2. 获取里面所有的方法对象
Method[] methods1 = clazz.getMethods();
for (Method method : methods1) {
System.out.println(method); // 会打印所有的公共方法,包括继承的(如Object类)
}

Method[] methods2 = clazz.getDeclaredMethods();
for (Method method : methods2) {
System.out.println(method); // 会打印所有的方法,包括继承的(如Object类)
}

// 3. 获取单个方法
Method eat = clazz.getDeclaredMethod("eat", String.class);
System.out.println(eat);

// 4. 获取方法的形参
Parameter[] parameters = eat.getParameters();
for (Parameter parameter : parameters) {
System.out.println(parameter); // java.lang.String arg0
}

// 获取方法的修饰符
int modifiers = eat.getModifiers();
System.out.println(modifiers);

// 获取方法的抛出的异常
Class[] exceptionTypes = eat.getExceptionTypes();
for (Class exceptionType : exceptionTypes) {
// class java.io.IOException
// class java.lang.NullPointerException
// class java.lang.ClassCastException
System.out.println(exceptionType);
}

// 方法运行
Student s = new Student();
eat.setAccessible(true);
Object res = eat.invoke(s, "炒饭");
System.out.println(res); // 吃完啦

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

import java.io.IOException;

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

public Student(){};

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

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public void sleep() {
System.out.println("睡觉");
}

private String eat(String something) throws IOException,NullPointerException,ClassCastException {
System.out.println("在吃" + something);
return "吃完啦";
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

反射的作用

  1. 获取一个类里面的所有信息,获取到了之后,再执行其他的业务逻辑
  2. 结合配置文件,动态的创建对象并调用方法

将对象的成员变量输出为文本文件:

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

import java.io.*;
import java.lang.reflect.Field;

public class myreflectdemo {
public static void main(String[] args) throws IllegalAccessException, IOException {
/*
将任意一个对象的所有字段名和值保存到文件中去
*/
Student s= new Student("张三",23,"男",177,"睡觉");
Teacher t= new Teacher("小王",10000);
saveObject(s);

}

public static void saveObject(Object obj) throws IllegalAccessException, IOException {
// 1. 获取字节码文件的对象
Class clazz = obj.getClass();

// 创建IO流
BufferedWriter bw = new BufferedWriter(new FileWriter("a.txt"));

// 2. 获取所有的成员变量
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
String name = field.getName();
Object val = field.get(obj);

// 写出数据
bw.write(name + "=" + val);
bw.newLine();
}

bw.close();
}
}

动态代理

特点:无侵入式的给代码增加额外的功能

flowchart TB
    A[调用者] --> B[代理]
    B --> C[对象]

Java中,通过接口保证,后面的对象和代理需要实现同一个接口

接口中就是被代理的所有方法

创建代理

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

参数一:用于指定用哪个类加载器,去加载生成的代理类

参数二:指定接口,这些接口用于指定生成的代理长什么样子,也就是有哪些方法

参数三:用来指定生成的代理对象要干什么事情

案例

类:

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

public class User implements Helper{
private String name;

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

public String getName() {
return name;
}

// 唱歌
@Override
public String sing(String name) {
System.out.println(this.name + "正在唱" + name);
return "谢谢";
}

// 跳舞
@Override
public void dance() {
System.out.println(this.name + "正在跳舞");
}

public void setName(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}

接口:

1
2
3
4
5
6
7
8
9
10
package mydynamicproxydemo1;

public interface Helper {
// 可以把所有想要被代理的方法定义在接口中
// 唱歌
public abstract String sing(String name);

// 跳舞
public abstract void dance();
}

代理方法:

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

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtil {
// 方法作用:给对象创建一个代理
// 形参:被代理的对象
// 返回值:给对象创建的代理

// 需求:
// 需要用户唱歌
// 1. 获取代理的对象
// 代理对象 = ProxyUtil.createProxy()
// 2. 再调用代理的唱歌方法
// 代理对象.唱歌的方法()
public static Helper createProxy(User u) {
/*
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

参数一:用于指定用哪个类加载器,去加载生成的代理类
参数二:指定接口,这些接口用于指定生成的代理长什么样子,也就是有哪些方法
参数三:用来指定生成的代理对象要干什么事情
*/

Helper helper = (Helper) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(), // 参数一:用于指定用哪个类加载器,去加载生成的代理类
new Class[]{Helper.class}, // 参数二:指定接口,这些接口用于指定生成的代理长什么样子,也就是有哪些方法
new InvocationHandler() { // 参数三:用来指定生成的代理对象要干什么事情
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 参数一:代理的对象
// 参数二:要运行的方法
// 参数三:调用方法时,传递的实参
if("sing".equals(method.getName())) {
System.out.println("准备话筒,收钱");
} else if ("dance".equals(method.getName())) {
System.out.println("准备场地,收钱");
}

return method.invoke(u,args);
}
}
);
return helper;
}
}

测试类:

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

public class Test {
public static void main(String[] args) {
// 1. 获取代理的对象
User u = new User("张三");
Helper proxy = ProxyUtil.createProxy(u);

// 2. 调用唱歌的方法
String res = proxy.sing("乱唱");
System.out.println(res);

// 3. 调用跳舞的方法
proxy.dance();
}
}