0%

Java SE

Java 入门整理的笔记

工具

JDK

JDK是Java开发工具包,其中包含javacjava两个程序,使用Java编程需要JDK。

Java编译运行

Java程序的编译运行是跨平台(指操作系统)的,主要归功于JVM,在Java程序的编译运行分为三步(编写、编译、运行。可直接使用命令行操作):

  1. 编写源代码
  2. .java文件(源代码)通过 javac.exe 程序(编译器)编译成.class文件
  3. .class文件(又称字节码文件)通过 java.exe (解释器)转换成机器码,原理是放入对应平台的Java虚拟机(JVM)内转化为计算机可运行二进制机器码

JRE

JRE是Java运行环境,其中包含了JVM,由上步骤可知,想要运行Java程序,必须要有JRE。在安装JDK时,JDK中已经自带了JRE,所以不需要单独安装JRE

为什么要配置环境变量

当你在命令行中使用 javacjava 时,需要先进入到jdk的安装目录中的 bin 文件目录下,才能运行Java命令,但是在命令行中运行记事本时,你只并不需要找到记事本的安装目录,直接输入notepad命令即可,这是因为计算机知道去哪里找记事本的目录,但是不知道去哪找Java的目录,为了能在命令行窗口中,直接运行Java,去掉繁琐的输目录的过程,所以我们需要在环境变量中添加路径。(即配置环境变量)

  1. 首先在系统变量栏添加一个 JAVA_HOME 来代替 bin 目录之前的一长串目录。
  2. 然后再在系统变量栏里找到 Path ,编辑一条%JAVA_HOME%\bin%JAVA_HOME%就是代替了 bin 之前的一长串目录而已。
  3. 这样我们就把Java的路径添加到了环境变量中,计算机在收到Java指令的时候就知道去Java的目录下去寻找Java程序了。

基础概念

关键字

完全小写的字母。 在增强版的记事本(notepad++)或者其他IDE当中有特殊颜色。

标识符

类、方法、变量的名称,自己取的那种,但是

要求: - 英文字母(区分大小写)、数字、$(不推荐,因为后面学到的内部类的class文件命名方式包含了此符号,见 23 内部类)和_ - 不能以数字开头 - 不能是关键字

建议: - 类名:大驼峰式 - 变量名:小驼峰式 - 方法名:小驼峰式

常量

程序运行期间固定不变的量

常量的分类: 1. 字符串常量:凡是用双引号引起来的部分,叫做字符串常量。 2. 整数常量:直接写上的数字,没有小数点。 3. 浮点数常量:直接写上的数字,有小数点。 4. 字符常量:凡是用单引号引起来的单个字符。叫做字符常量。(Java中 一个中文也是一个字符)两个单引号中间必须有且仅有一个字符,没有不行;有两个也不行。 5. 布尔常量:true、false 6. 空常量:null 代表没有任何数据 不能直接用来打印输出

变量

与常量相反。程序运行期间,内容可以发生改变的量。注意:变量赋值时,右侧数值范围不能超过左侧的数据范围 ,long与float赋值带后缀L与F

注意事项: 1. 如果创建多个变量,那么变量之间的名称不可以重复 2. 对于 floatlong 类型来说,字母后缀F和L不要丢掉 3. 如果使用 byte 或者 short 类型的变量,右侧的数据值,不能超过左侧类型的范围 4. 没有进行赋值的变量不能直接使用,一定要赋值之后才能使用 5. 变量的使用不能超过作用域的范围。作用域: 从定义变量的一行开始,一直到直接所属的大括号结束为止 6. 可以通过一个语句来创建多个变量,但是一般情况下不推荐。

数据类型

基本数据类型

四类八种: - 整数型 byte short int long - 浮点型 float double 但是浮点数往往会有精度损失。所以在定义货币的时候,没有1.11元的说法,往往都是111分。详情请看《计算机组成原理》中存储浮点数的底层原理 - 字符型 char - 布尔型 boolean

数据类型 关键字 内存占用 取值范围
字节型 byte 1个字节(8位) -128~127 ($ -2727-1 $)
短整型 short 2个字节 -32768~32767
整型 int(默认) 4个字节 $ -2{31}2{31}-1$(大约21个亿)
长整型 long 8个字节 $ -2{63}2{63}-1$
单精度浮点数 float 4个字节 1.4013E-45~3.4028E+38
双精度浮点数 double(默认) 8个字节 4.9E-324~1.7977E+308
字符型 char 2个字节 0-65535
布尔类型 boolean 1个字节 true false

注意事项: 1. 字符串不是基本类型,而是引用类型 2. 浮点型可能只是一个近似值,并非精确的值 3. 数据范围与字节数不一定相关, float(4字节)数据范围比 long(8字节)更加广泛。 4. 浮点数当中默认类型是 double 。如果一定要用 float ,需要加上一个后缀 F。如果是整数,默认为 int 类型,如果一定要用 long ,需要加上一个后缀 LF L 大小写都可以,推荐大写

引用数据类型

不是基本数据类型 就是 引用数据类型 只有这两种。比如后面要介绍的字符串、数组、类、接口、Lambda等

数据类型转换

当数据类型不一样时,将会发生数据类型转换

一句话概括: 对于一行赋值语句某个变量 = 某个变量/常量 首先如果右边是常量,判断常量的值是否超过左边的类型对应的数值范围,若超过了,直接报错! 然后判断左右两边类型的数值范围大小,若由小到大(右边小,左边大),可自动转。否则,需要强转!

  • 自动类型转换(隐式)
  1. 特点:代码不需要进行特殊处理,自动完成
  2. 规则:数据范围从小到大,与字节数不一定相关 数据范围规则: byte、short、char --> int --> long --> float --> double
  • 强制类型转换(显式)
  1. 特点:代码需要进行特殊的格式处理,不能自动完成。
  2. 格式: 范围小的类型 范围小的变量名 = (范围小的类型)原本范围大的数据

注意事项: 1. 强制类型转换一般不推荐使用,因为有可能发生精度损失、数据溢出。 2. byte/short/char 这三种类型都可以发生数学运算。 3. byte/short/char 在运算的时候,都会被首先提升为int类型,然后再计算。4.2和4.6的代码块都有用到 4. 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
public class Demo01DataType{
public static void main(String[] args) {
System.out.println(1024); // 这就是一个整数,默认就是int类型
System.out.println(3.14); // 这就是一个浮点数,默认就是double类型

// 左边是long类型,右边是默认的int类型,左右不一样
// int --> long long的数据范围比较大,符合了数据范围从小到大的要求
// 这一行代码发生了自动类型转换
long num1 = 100;
System.ou.println(num1); //100

//左边是double类型,右边是float类型,左右不一样
//float --> double,符合数据从小到大的规则
// 也发生了自动类型转换
double num2 = 2.5F;
System.out.println(num2); //2.5

//左边是float类型,右边是long类型
//long --> float 范围float跟更大。符合从小到大的规则
// 自动类型转换
float num3 = 30L
System.out.println(num3); //30.0
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo02DataType {
public static void main(String[] args) {
//左边是int类型,右边是long类型,不一样
//long --> int, 不是从小到大
//不能自动类型转换 进行强制转换
int num = (int)100L;
System.out.println(num);

// long强制转换成int类型 数据溢出
int num2 = (int)6000000000L;
System.out.println(num2); //1705032704

// double --> int,强制类型转换 精度损失
int num3 = (int)3.99;
System.out.println(num3); //3 并不是四舍五入,所有小数位都会被舍弃

char zifu1 = 'A';
System.out.println(zifu1 + 1); // 66 大写字母A被当作65进行处理
//计算机的底层会用一个数字(二进制)来代表字符A,就是65

byte num4 = 40; //右侧的数值大小不能超过左侧的类型范围 40 < 127 不会报错
byte num5 = 50;
//byte + byte --> int + int --> int -->byte 大范围变小范围,需要强制类型转换
byte result1 = (byte)(num4 + num5);
System.out.println(result1); //90
}
}

编码表

数字和字符的对照关系表(编码表): - ASCII: American Standard Code for Information Interchange 美国信息交换标准代码,48代表'0';65代表大写字母'A';97代表小写字母'a' ;一个中文占2个字节 - Unicode 万国码,开头0-127部分和ASCII完全一样,但是从128开始包含有更多字符。其中 UTF-8一个中文占3个字节

运算符

运算符:进行特定操作的符号 表达式:用运算符连起来的式子

算数运算符

  • 四则运算符:加减乘除(`+ - * /) 对于整数表达式来说,除法/`用的是整除,只看商,不看余数
  • 取模运算(取余数):%
  • 自增运算符:++ ;自减运算符:--
    • 单独使用时,前++后++没有任何区别
    • 混合使用时:
      • 如果是前++,立刻让变量+1,然后拿着结果进行使用
      • 如果是后++,首先使用变量本来的数值,然后再让变量+1

注意事项: 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
public class Demo06operator {
public static void main(String[] args) {
int num1 = 10;
System.out.println(num1); // 10
++num1; // 单独使用,前++
System.out.println(num1); // 11
num1++; // 后++
System.out.println(num1) // 12
System.out.println("============");

// 与打印操作混合
int num2 = 20;
System.out.println(++num2); // 21 先++
System.out.println("============");

int num3 = 30;
System.out.println(num3++); // 30 后++,首先使用变量本来的30,然后再让变量+1
System.out.println(num3); // 31

int x = 10;
int y = 20;
// 11 + 20
int result3 = ++x + y--;
System.out.println(result3); // 31
System.out.println(x); // 11
System.out.println(y); // 19
}
}

赋值运算符

分为:基本赋值运算符=和复合赋值运算符+= -= *= /= %=

注意事项: 1. 只有变量才能使用赋值运算符,常量不能进行赋值 2. 复合赋值运算符其中隐含了一个强制类型转换

1
2
3
4
5
6
7
8
byte num = 30;
// byte类型在执行运算时会先转换成int
// num = num + 5
// num = byte + int
// num = int
// num = (byte)int 这里隐含了一个强制转换
num += 5;
System.out.println(num); // 35

比较运算符

== < > <= >= !=变量与变量之间,常量与常量之间,变量与常量之间,都可以使用。

注意事项: 1. 比较运算符的结果一定是一个boolean值,成立就是true,不成立就是false。 2. 如果进行多次判断,不能连着写。 3. 关于== 详情见 Object 类中的 equals 方法

逻辑运算符

短路与(并且) && 短路或(或者) || 非(取反) ! &&||具有短路效果,节省性能。

注意事项: 1. 逻辑运算符只能用于boolean值 2. 与、或需要左右各有一个boolean。取反只需要一个 3. 与、或两种运算符,如果有多个条件,可以连续写

三元运算符

格式: 数据类型 变量名称 = 条件判断 ? 表达式A : 表达式B;

首先判断条件是否成立: - 如果成立为true,将表达式A的值赋值给左侧的变量 - 如果不成立为false,将表达式B的值赋值给左侧的变量 二者选其一

注意事项: 1. 必须同时保证表达式A和表达式B都符合左侧的数据类型的要求。 2. 三元运算符的结果必须被使用,要么赋值,要么直接输出。

编译器优化

  1. 对于byte short char类型赋值,如果右侧赋值数值没有超过范围,编译器自动补上强转;若超过范围,编译器报错。
  2. 在给变量进行赋值的时候,如果右侧的表达式当中全都是常量,没有任何变量。那么编译器会直接将若干个常量表达式计算得到结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
short a = 5; // 5 没有超过 short的范围,正确 a是short型
short b = 8; // 正确 b是short型
// 65没有超过char的数值范围;
// int --> char 数值范围又大变小 不能自动转,需要强转
// 编译器自动补上一个隐含的(char)
char zifu = /*(char)*/ 65;
System.out.println(zifu); // A
// short在执行运算时自动转换成int型,运算结果自然是int型
//short + short --> int + int = int
shrot result = a + b; //编译报错!左侧是short 右侧结果是int 缺少强制转换!

short result = /*(short)*/ (5 + 8); //编译器优化功能,右侧全是常量,可以直接计算结果,计算完结果后,可以自动补上强转(short)
System.out.println(result);

short result2 = 5 + a + 8; //编译器报错!右侧有变量,不能直接计算,int转short需要强转

上述记录于2019.10.14

流程控制

需要清楚每条语句的执行流程,即各条语句的执行顺序

顺序结构

这个就不解释了。。。。

判断结构

  1. if语句第一种格式:if
1
2
3
if(关系表达式) {
语句体;
}
  1. if语句第二种格式:if...else
1
2
3
4
5
if(关系表达式) {
语句体1;
} else {
语句体2;
}
  1. if语句第三种格式:if...else if...else
1
2
3
4
5
6
7
8
9
10
11
if (判断条件1) {
执行语句1;
} else if (判断条件2) {
执行语句2;
}
...
} else if (判断条件n) {
执行语句n;
} else {
执行语句n+1;
}

选择结构

选择语句: switch

1
2
3
4
5
6
7
8
9
10
11
12
switch(表达式) {  // 括号里就是一个表达式,计算结果不一定是一个boolean值
case 常量值1:
语句体1;
break; //遇到break 结束
case 常量值2:
语句体2;
break;
...
default: // 所有的case都和表达式结果不匹配,就执行default
语句体n+1;
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
32
33
/*
switch 语句使用的注意事项
1.多个case后面的数值不可以重复
2.switch后面小括号当中只能是下列数据类型:
基本数据类型:byte/short/char/int
引用数据类型:String/enum以及上述四个基本数据类型的包装类
3.switch语句格式可以很灵活:前后顺序可以颠倒,而且break语句还可以省略
匹配哪一个case就从哪一个位置向下执行,知道遇到了break或者整体结束为止。
*/
public class Demo08SwitchNotice {
public static void main(String[] args) {
int num = 2;
switch (num) {
case 2: // 1 2 先后顺序可以颠倒
System.out.println("你好");
// break 可以省略 省略之后,遇到下一个case直接“穿透” 执行下面的语句。
case 1: //穿透case
System.out.println("我好"); // 继续执行
break; // 结束 最后的输出结果 “你好 \回车 我好”
/*
case 1:
System.out.println("他好"); // 编译报错,case后面的值不能重复
break;
*/
default:
System.out.println("大家好");
break;
case 3:
System.out.println("我也好");
break;
}
}
}

循环结构

循环结构的基本组成部分,一般可以分成四部分: 1. 初始化语句:在循环开始最初执行,而且只做唯一一次。 2. 条件判断: 如果成立,则循环继续;如果不成立,则循环退出。 3. 循环体:重复要做的事情内容,若干行语句。 4. 步进语句:每次循环之后都要进行的扫尾工作,每次循环结束之后都要执行一次。

  1. 循环语句1: for
1
2
3
for (初始表达式;布尔表达式;步进表达式) {
循环体;
}
  1. 循环语句2:while
1
2
3
4
5
初始表达式;
while (布尔表达式) {
循环体;
步进表达式;
}
  1. 循环语句3: do-while
1
2
3
4
5
初始化表达式;
do { // 第一次是无条件执行,不管循环条件是否成立,至少执行一次
循环体;
步进表达式;
} while (布尔表达式);

三种循环的区别: - 如果条件判断从来没有满足过,那么for 循环和wile 循环将会执行0次,但是do-while 循环会执行至少一次 - for 循环的变量在小括号当中定义,只有循环内部才可以使用。while 循环和do-while 循环初始化语句本来就在外面,所以出来循环之后还可以继续使用。

小建议:凡是次数确定的场景,多用for循环;否则多用while循环

  1. 死循环 永远停不下来的循环,编译可以通过,可以运行,有时会使用到
1
2
3
while (true) {
循环体;
}

但是死循环后面加上的语句无法执行,会报错!

1
2
3
4
5
6
7
8
public class Demo16DeadLoop {
public static void main(String[] args) {
while (true) {
System.out.println("Hello, World!");
} // 死循环可以正常编译,可以运行!
System.out.println("Hello!"); // 编译报错! 死循环结束不了,后面的语句执行不到。
}
}
  1. 循环的嵌套 很好理解,循环内部写一个循环。

条件控制

  1. break语句

break的用法有常见的两种: 1. 可以用在switch语句到那个中,一旦执行,整个switch语句立刻结束 2. 还可以用在循环语句当中,一旦执行,整个循环语句立刻结束。打断循环。

  1. continue语句

一旦执行,立刻跳过当前次循环剩余内容,马上开始下一次循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo15Continue {
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
if (i == 4) { //如果当前是第四层
continue; //那么跳过当前次循环,马上开始下一次循环(第5层)
}
System.out.println(i + "层到了。");
/*
输出:
1层到了。
2层到了。
3层到了。
5层到了。
6层到了。
7层到了。
8层到了。
9层到了。
10层到了。
*/
}
}
}

上述记录于2019.10.15

# IDE

方便java开发的软件。 存储结构:一个项目project ---(多个)模块module ----(多个)包package ---- 文件(可以是包,可以是java文件)

常用的Intelij IDEA 快捷键 : alt + 空格 导入包 ctrl + Y 删除当前行 ctrl + D 复制当前行 ctrl + alt + L 格式化 ctrl + / 添加注释 ctrl + shift + / 多行注释 alt + ins 自动生成代码 alt + shift + 上下箭头 移动代码行 alt + 回车 自动添加代码!很常用!我经常用它来 Introduce local variable

# 方法

将一个功能抽取出来,把代码单独定义在一个大括号内,形成一个单独的功能 当我们需要这个功能时,就可以去调用。这样即实现了代码的复用性,也解决了代码冗余的现象。

方法的概念

方法其实就是若干语句的功能集合 方法好比一个工厂。 蒙牛工厂 原料:奶牛、饲料、水 产出物:奶制品 钢铁工厂 原料:铁矿石、煤炭 产出物:钢铁建材 参数(原料):进入方法的数据 返回值(产出物):从方法中出来的数据

定义方法

格式:

1
2
3
4
修饰符 返回值类型 方法名称(参数类型 参数名称, ...) {    
方法体
return 返回值;
}

修饰符: 现阶段的固定写法,public static 返回值类型: 也就是方法最终产生的数据结果是什么类型 方法名称: 方法的名字,规则和变量一样. 小驼峰 参数类型:进入方法的数据是什么类型 参数名称:进入方法的数据对应的变量名称 PS: 参数如果有多个,使用逗号进行分隔 方法体:方法需要做的事情,若干行代码 return: 两个作用。1. 停止当前方法 2.将后面的结果数据(返回值)还给调用处 返回值:也就是方法执行后最终产生的数据结果 return 后面的返回值,必须和方法名称前面的返回值类型,保持对应

方法的调用格式

  1. 单独调用: 方法名称(参数);
  2. 打印调用: System.out.println(方法名称(参数));
  3. 赋值调用: 数据类型 变量名称 = 方法名称(参数);

注意:无返回值的方法,只能进行单独调用!

方法的调用流程

  1. 找到方法
  2. 参数传递
  3. 执行方法体
  4. 若有返回值,带着返回值回到方法的调用处

方法定义的注意事项

  1. 方法应该定义在类当中,不能在方法中再定义方法。不能嵌套
  2. 方法定义的前后顺序无所谓
  3. 方法定义之后不会执行,如果希望执行,一定要调用:单独调用、打印调用、赋值调用
  4. 如果方法有返回值,必须写“return 返回值;” 不能没有
  5. return后面的返回值数据,必须和方法的返回值类型 对应起来
  6. 对于一个void没有返回值的方法,不能写return后面的返回值, 只能写return自己。
  7. 对于void方法当中最后一行的return可以省略不写
  8. 一个方法当中可以有多个return语句,但是必须保证同时只有一个会被执行到。
  9. 两个return不能连写

Overload 重载

本来可以归并到方法里讲的,但是我给他单独列出来,因为它的确很重要,而且很容易和后面的方法覆盖重写 Override 搞混淆。

  • 为什么使用方法重载: 对于功能类似的方法来说,因为参数列表不一样,却要记住那么多不同的方法名称,太麻烦 可不可以只需要记住一个方法名称,就可以实现类似的多个功能呢?

  • 方法的重载(Overload): 多个方法的名称一样,但是参数列表不一样。

  • 方法重载与下列因素相关:

  1. 参数个数不同 (可变参数在参数类型确定、个数不确定时使用)
  2. 参数类型不同
  3. 参数的多类型顺序不同
  • 方法重载与下列因素无关
  1. 参数名称
  2. 方法的返回值类型
1
2
3
4
5
6
7
8
public static void open() {...}  // 原始方法
public static void open(int a) {...} // 正确重载
static void open(int a, int b) {...} // 代码错误 和第8行冲突 重载和修饰符没关系
public static void open(double a, int b) {...} // 正确重载
public static void open(int a, double b) {...} // 代码错误 和第6行冲突 重载和参数名称无关
public void open(int i, double d) {...} // 代码错误 和第5行冲突
public static void OPEN() {...} // 代码正确不会报错,但是并不是有效重载 因为方法名字不一样!
public static void open(int i, int j) {...} // 代码错误 和第3行冲突

上述记录于2019.10.16

数组

是一种容器,可以同时存放多个数据值。

  • 数组的特点:
  1. 数组是一种引用类型 (所以直接打印的话出现的是数组的地址值)
  2. 数组中的多个数据类型必须统一
  3. 数组的长度在程序运行期间不可改变 (2、3也是和集合的区别所在)
  • 数组的初始化: 在内存中创建一个数组,并且向其中赋予一些默认值
  1. 动态初始化:(指定长度)
  2. 静态初始化:(指定内容)
  • 动态初始化格式
1
2
3
4
数据类型[] 数组名称 = new 数据类型[数组长度]
数组名称[0] = 要存的元素1
数组名称[1] = 要存的元素2
...

如果是整数类型,默认为0 如果是浮点类型,默认为0.0 如果是字符类型,默认为'\u0000' 一种特殊的字符,不是空但是打印不出来 如果是布尔类型,默认为false 如果是引用类型,默认为null 可以打印出来

  • 静态初始化格式
1
2
数据类型[] 数组名称 = new 数据类型[] {元素1,元素2,...};
数据类型[] 数组名称 = {元素1,元素2,...};

虽然没指定长度,但是可以根据大括号里的元素自动推算出长度。 也有默认值,只不过系统自动马上将默认值替换为了大括号当中的具体数值。

  • 使用建议: 如果不确定数组当中的具体内容,用动态初始化;否则用静态初始化。

  • 常见问题:

  1. 如果访问数组元素的时候,索引编号并不存在,将会发生数组索引越界异常:ArrayIndexOutOfBoundsException
  2. 数组必须进行new初始化才能使用其中的元素。如果只是赋值了一个null,没有进行new,将会放生空指针异常:NullPointerException
  3. 将数组当作方法的参数时,方法接收的是数组的地址。
  4. 当方法返回参数是数组时,方法返回的是数组的地址。(3、4两点会在11中详细解释!)
  • 缺点: 数组一旦创建,程序运行期间,长度不可改变

Java的内存划分

5个部分 1. 栈(Stack): 存放的都是方法中的局部变量。方法的运行一定要在栈当中运行 局部变量:方法的参数,或者是方法{}内部的变量 作用域:一旦超出作用域,立刻从占内存种消失

  1. 堆(Heap): 凡是new出来的东西,都在堆当中 成员变量:类中的定义的变量,在方法外边 字符串常量池 详情见14.1 字符串常量池 堆内存里面的东西都有一个地址:16进制 堆内存里面的数据都有默认值:规则同数组。

  2. 方法区(Method Area): 存储 .class相关信息,包含方法的信息、常量、静态变量(static)

  3. 本地方法栈(Native Method Stack): 与操作系统相关

  4. 寄存器(pc Register): 与CPU相关

Java 内存划分

上述记录于2019.10.19

面向对象

面向过程: 当需要实现一个功能的时候,每一个具体的步骤都要亲力亲为,详细处理每一个细节。

面向对象: 当需要实现一个功能的时候,不关心具体步骤,而是找一个已经具有该功能的人,来帮我做事儿。 面向对象思想是一种更符合我们思考习惯的思想,它可以将复杂的事情简单化,并将我们从执行者变成了指挥者。

特点: 三大基本特征:封装、继承和多态。

类和对象

  • 类: 是一组相关属性和行为的集合。可以看成是一类事物的模板
    • 属性:是什么
    • 行为:能做什么
  • 对象:是一类事物的具体体现。

类和对象的关系

  • 类是对一类事物的描述,是抽象的
  • 对象是一类事物的实例,是具体的
  • 类是对象的模板,对象是类的实体

类的定义

成员变量(属性) 成员方法(行为)

注意事项: 1. 成员变量是直接定义在类当中的,在方法外边 2. 成员方法不要写 static 关键字(静态方法是类的方法)

对象的创建

通常情况下,一个类并不能直接使用,需要根据类创建一个对象,才能使用 1. 导包:也就是指出需要使用的类在什么位置 import 包名称,类名称; 对于和当前类属于同一个包的情况,可以省略导包语句不写 2. 创建格式: 类名称 对象名 = new 类名称(); 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
package cn.perdant.day06.demo03;

/*
局部变量和成员变量
1. 定义的位置不一样
局部:在方法内部
成员:在方法外部,直接写在类当中
2. 作用范围不一样
局部:只有方法当中才可以使用,出了方法就不能再使用
成员:整个类全部都可以通用
3. 默认值不一样
局部:无默认值,如果要使用,必须手动进行赋值
成员变量:如果没有赋值,会有默认值,规则和数组一样
4. 内存的位置不一样
局部:位于栈内存
成员:位于堆内存
5. 生命周期不一样
局部:随着方法进栈而诞生,随着方法出栈而消失
成员:随着对象创建而诞生,随着对象被垃圾回收而消失
*/
public class Demo01VariableDifference {
String name;
public void methodA() {
int num = 20;
System.out.println(num);
System.out.println(name);
}
public void methodB(int param) { // 方法的参数就是局部变量
// System.out.println(num); 报错 这是methodA方法的局部变量,已经超过了作用域
// 参数在方法调用的时候,必然会被赋值!所以不会报错!
System.out.println(param);
int age;
// System.out.println(age); 报错 局部变量没赋值不能用
System.out.println(name); // 成员变量 虽然没赋值但是有默认值
}
}

封装

  1. 方法是一种封装
  2. 关键字 private 也是一种封装 封装就是将一些细节信息隐藏起来,对于外界不可见。

private关键字

一旦使用 private 进行修饰,那么本类当中仍然可以随意访问,但是超出了本类范围之外就不能再直接访问。 间接访问 private 成员变量,定义一堆 Getter/Setter 方法 必须叫 setXXX 或者 getXXX 命名规则 Getter 不能有参数,返回值类型和成员变量对应。 Setter 不能有返回值,参数类型和成员变量对应。

this关键字

当方法的局部变量和类的成员变量重名的时候,根据“就近原则”,优先使用局部变量。 如果需要访问本类当中的成员变量,需要使用格式: this.成员变量 哪个对象调用的方法,方法里的this就是那个对象

上述记录于2019.10.21

构造方法

构造方法是专门用来创建对象的方法,当我们通过关键字new来创建对象时,其实就是在调用构造方法。

1
2
3
public 类名称(参数类型 参数名称) {
方法体
}

注意事项: 1. 构造方法的名称必须和所在的类名称完全一样,就连大小写都要一样 2. 构造方法不要写返回值类型,连void都不写 3. 构造方法不能return 一个具体的返回值 4. 如果没有编写任何构造方法,那么编译器将会默认赠送一个构造方法,没有参数、方法体什么事情都不做 5. 一旦编写了至少一个构造方法,那么编译器将不再赠送 6. 构造方法也是可以进行重载的

一个标准的类

  1. 所有的成员变量都要使用 private 关键字修饰
  2. 为每一个成员变量编写一对儿 Getter/Setter 方法
  3. 编写一个无参数的构造方法
  4. 编写一个全参数的构造方法 标准类被称为Java Bean

上述记录于2019.10.22

匿名对象

只有右边的对象,没有左边的名字和赋值运算符

1
new 类名称();

注意事项: 1. 匿名对象只能使用唯一的一次,下次再用不得不再创建一个新对象。 2. 如果确定有一个对象只需要使用唯一的一次,就可以用匿名对象。

常用API (一)

Application Programing Interface 应用程序编程接口是JDK中提供给我们直接使用的类 API文档就是程序员的字典 主要看 包路径、构造方法、方法摘要

引用类型的一般使用步骤: 1. 导包 只有java.lang 包下的内容不需要导包,其他的包都需要 import 语句 2. 创建 3. 使用

Scanner

一个可以解析基本类型和字符串的文本扫描器。可以实现键盘输入数据,到程序当中。 new Scanner(System.in) 生成一个Scanner对象,并接收输入 int nextInt() :返回你所输入的整型数 String next() :返回你所输入的字符串 详情参见API

上述记录于2019.10.25

Random

Random类用来生成随机数字。 nextInt(无参数):获取一个随机的int数字(范围是int所有范围,有正负) nextInt(有参数):获取一个随机的int数字(参数代表了范围,左闭右开区间) 详情参见API

1
2
Random r = new Random();
int anInt = r.nextInt(5); // 左闭右开 [0,5)

ArrayList

前面说过,数组的长度不可以发生改变。(详情见10) 但是集合ArrayList<E>的长度是可以随意变化的。(后面还会介绍更多种集合) <E> 代表泛型:也就是装在集合当中的所有元素,全都是统一的什么类型,只能是引用类型,不能是基本类型。 从JDK 1.7+开始,右侧的尖括号内容可以不写,但是<>本身还是要写。 ArrayList<String> arrayList = new ArrayList<>();

注意事项: 1. 对于ArrayList集合来说,直接打印得到的不是地址值,而是内容(说明它覆盖重写了toString方法) 2. 如果内容是空的,得到的是空的中括号:[] 详情参见API

1
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 cn.perdant.day07.demo04;

import java.util.ArrayList;

/*
ArrayList当中的常用方法:
public boolean add(E e): 向集合当中添加元素
public E get(int index):集合当中获取元素
对于ArrayList集合来说,add添加动作一定成功,返回值可以不用
但是其他集合不一定成功(有的集合不能添加重复的元素,后面会学到)
public E remove(int index):从集合当中删除元素
public int size():获取集合的尺寸长度
*/
public class Demo03ArrayListMethod {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
System.out.println(list);

//向集合当中添加元素:add
boolean success = list.add("五六七");
System.out.println(list);
System.out.println("添加的动作是否成功" + success);
list.add("梅十三");
list.add("鸡大保");
list.add("小飞");
list.add("江主任");
System.out.println(list);

//从集合当中获取元素
String name = list.get(2);
System.out.println("第2号索引位置" + name);

//从集合中删除元素
String whoRemoved = list.remove(4);
System.out.println("被删除的人是:" + whoRemoved);

//获取集合的长度尺寸
int size = list.size();
System.out.println("集合的长度是 " + 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
package cn.perdant.day07.demo04;

import java.util.ArrayList;

/*
如果希望向集合ArrayList当中存储基本类型数据,必须使用基本类型对应的包装类
包装类是引用类型,都位于java.lang包下
从JDK 1.5+开始,支持自动装箱、自动拆箱:基本类型--> 包装类型、包装类型-->基本类型
*/
public class Demo05ArrayListBasic {
public static void main(String[] args) {

ArrayList<String> listA = new ArrayList<>();
//错误写法!泛型只能是引用类型,不能是基本类型 因为ArrayList存储的是一个地址,基本类型,没有地址
//ArrayList<int> listB = new ArrayList<int>();
ArrayList<Integer> listC = new ArrayList<>();
listC.add(100);
listC.add(200);
System.out.println(listC);
int num = listC.get(1);
System.out.println("第一号元素是 " + num);

}
}

上述记录于2019.10.26

String 字符串

java.lang.String类代表字符串。 程序当中所有的双引号字符串,都是String类的对象。(就算没有new,也照样是) 字符串是常量;它们的值在创建之后不能更改。

字符串的特点: 1. 字符串的内容永远不可变 2. 因为字符串内容不可改变,所以可以共享使用 3. 字符串效果上,相当于是char[]字符数组,但是底层原理是byte[]字节数组

创建字符串的常见3+1种方式: - 三种构造方法 1. public String(); 创建一个空白字符串,不含有任何内容 2. public String(char[] array); 根据字符数组的内容。来创建对应的字符串 3. public String(byte[] array); 根据字节数组的内容来创建对应的字符串 - 一种直接创建 String str = "Hello"; // 右边直接用双引号 没有new,直接写上双引号,就是字符串对象

字符串常量池

从JDk1.7+ 常量池在堆内存中。注意:直接双引号的字符串,去常量池里找,没有则在常量池里生成,new的不去常量池!!直接在常量池外new。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
字符串常量池:
程序当中直接写上的双引号字符串,就在字符串常量池中。
new的不在常量池当中
对于基本类型来说,== 是进行数值的比较
对于引用类型来说,== 是进行地址值的比较
*/
public class Demo02StringPool {
public static void main(String[] args) {
String str1 = "abc";
String str2 = "abc";

char[] charArray = {'a','b','c'};
String str3 = new String(charArray);

System.out.println(str1 == str2); // true 都在常量池里,地址相同
System.out.println(str1 == str3); // false 一个在常量池,一个在池外new,地址不同
System.out.println(str2 == str3); // false 同上
}
}
字符串常量池

== 是进行对象的地址值比较,如果确实需要字符串的内容比较,可以使用两种方法:

  1. public boolean equals(Object obj);
    • 任何对象都能用Object进行接收
    • 具有对称性,a.equals(b) 与 b.equals(a) 效果一样
    • 如果比较双方一个常量一个变量,推荐把常量字符串写在前面
1
2
3
String str = null;
"Hello".equals(str); // 推荐常量写在前面 false
str.equals("Hello"); //不推荐常量写在后面,这种情况会报错,空指针异常,NullPointerException
  1. public boolean equalsIgnoreCase(String str); 忽略大小写,进行内容比较 只有英文字母区分大小写其他都不区分大小写

String常用方法

String 当中与转换相关的常用方法: public char[] toCharArray() 将当前字符串拆分成为字符数组作为返回值 public byte[] getBytes() 获得当前字符串底层的字节数组 public String replace(CharSequence oldString, CharSequence newString) 将所有出现的老字符串替换成新的字符串,返回替换之后的结果新字符串 CharSequence 是个接口,意思就是说,可以接收字符串类型

String 当中与获取相关的常用方法: public int length() 获取字符串长度 public String concat(String str) 拼接字符串 public char chaAt(int index) 获取指定索引位置的单个字符 public int indexOf(String str) 查找参数字符串在本字符串当中首次出现的索引位置,如果没有返回-1

字符串的截取方法: public String substring(int index) public String substring(int begin, int end) 左闭右开 [begin, end)

分隔字符串的方法: public String[] split(String regex) 按照参数的规则,将字符串切分成若干部分 regex参数其实是一个正则表达式

上述记录于2019.10.27

静态(static 关键字)

一旦用了static 关键字 那么这样的内容不在属于对象自己而是属于类的 凡是本类的对象,都共享同一份 无论是成员变量还是成员方法,一旦使用了static,都推荐使用类名称进行调用 如果是用对象名称调用,编译器在编译时会自动转换成类名称调用

注意事项: 1. 静态不能直接访问非静态 原因:内存当中是先有静态内容,后有非静态内容 2. 静态方法当中不能用this 原因:this代表当前对象。但静态与对象没有关系,只和类有关系。 3. 关于静态代码块:

1
2
3
4
5
public class 类名称 {
static {
// 静态代码块的内容
}
}
  • 当第一次用到本类时,静态代码块执行唯一的一次
  • 静态内容总是优先于非静态,所以静态代码块比构造方法先执行
  • 静态代码块的典型用途,用来一次性地对静态成员变量进行赋值

常用工具类

下面介绍的几个工具类,都提供了大量的静态方法, 所以不需要new对象,直接可以通过类名调用

Arrays

一个与数组相关的工具类,提供了大量静态方法。用来实现数组的常见操作。 public static String toString(数组) 将参数数组编程字符串,按照默认格式[元素1,元素2,元素3,...] public static void sort(数组) 按照默认升序(从小到大) 对数组的元素进行排序 如果是数值,默认升序 从小到大 如果是字符串 ,默认按照字母升序 如果是自定义类 这个自定义类需要有Comparable或者Cmoparator接口支持(后面要讲的覆盖重写)

Math

是数学相关的工具类,里面提供了大量的静态方法,完成与数学相关的操作

1
2
3
4
5
public static double abs(double num); //绝对值
public static double ceil(double num); // 向上取整
public static double floor(double num); // 向下取整
public static long round(double num); //四舍五入
Math.PI 代表近似的圆周率常量

Collections

是集合相关的工具类,里面提供了大量的静态方法,完成与集合相关的操作详情见Collections类

上述记录于2019.10.28

继承

继承主要解决的问题就是:共性抽取

继承关系当中的特点: 1. 子类可以拥有父类的内容 2. 子类还可以拥有自己专有的内容

成员变量重名时:

在父子类的继承关系当中,如果成员变量重名,则创建子类对象时,访问有两种方式: 1. 直接通过子类对象访问成员变量 - 等号左边(创建对象时的赋值等号)是谁,就优先用谁,没有则向上找 2. 间接通过成员方法访问成员变量 - 方法属于谁,就优先用谁,没有则向上找 多态口诀:编译看左边,运行还看左边(后面会讲多态)

局部变量和成员变量重名时:

局部变量、本类的成员变量、父类的成员变量重名时如何区分?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Fu {
int num = 10;

}

public class Zi extends Fu {
int num = 20;
public void method() {
int num = 30;
System.out.println(num); // 30 就近原则 局部变量
System.out.println(this.num); // 20 本类变量
System.out.println(super.num); // 10 父类变量
}
}

成员方法重名时:

在父子类的继承关系当中,创建子类对象,访问成员方法的规则: 看右边创建的对象(new 的)是谁,就优先用谁的成员方法,如果没有则向上找。 (多态口诀: 编译看左边,运行看右边!)

1
2
3
4
Fu obj = new Zi();
obj.method(); // 编译看左,左边Fu中有,运行看右,右边Zi中有,调用Zi中的method方法
obj.methodFu(); //编译看左,左边Fu中有,运行看右,右边Zi中没有,向上找,调用Fu中的methodFu方法
obj.methodZi(); //编译报错,根据口诀,编译看左,左边是Fu,Fu当中没有methodZi方法!

注意事项: 1. Java语言是单继承的:一个类的直接父类只能由唯一一个。 2. Java语言可以多级继承。最顶端:java.lang.Object。 3. 一个子类的直接父类是唯一的,但是一个父类可以拥有很多个子类。 4. 无论是调用成员方法还是成员变量,如果没有都是向上找父类,绝对不会向下找子类。

上述记录于2019.10.29

Override(覆盖)重写

概念:在继承关系当中,方法的名称一样,参数列表也一样,也叫覆盖重写 而反观重载(参见重载):方法名称一样,参数列表不一样

方法的覆盖重写特点:创建的是子类对象,优先用子类方法

注意事项: 1. 必须保证父子类之间的方法名称相同,参数列表也相同。 - 可选的安全检测手段 @override :写在方法前面,用来检测是不是有效的正确覆盖重写 2. 子类方法的返回值,必须小于等于父类方法的返回值 3. 子类方法的权限,必须大于等于父类方法的权限修饰符 - public > protected > (default) > private - (default)不是关键字default, 而是什么都不写,留空!

super关键字

前面已经用到过,super关键字用来访问父类内容。

super关键字的三种用法: 1. 在子类的成员方法中,访问父类的成员变量 2. 在子类的成员方法中,访问父类的成员方法 3. 在子类的构造方法中,访问父类的构造方法

继承关系中,父子类构造方法的访问特点: 1. 子类构造方法当中有一个默认隐含的 super(); 调用,所以一定是先调用父类构造方法,后执行的子类构造方法 2. 子类构造可以通过super 关键字,调用父类重载构造 3. super()的父类构造调用,必须是子类构造方法的第一个语句,不能一个子类构造调用多次super构造

this关键字

this关键字用来访问本类内容,用法也有三种: 1. 在本类的成员方法中,访问本类的成员变量 2. 在本类的成员方法中,访问本类的另一个成员方法 3. 在本类的构造方法中,访问本类的另一个构造方法 - this(...)调用也必须是构造方法的第一个语句,也是唯一一个this。 此时super();不会赠送。 - superthis两种构造调用,不能同时使用。

抽象

抽象方法

如果父类当中的方法不确定如何进行{}方法体实现,那么这就应该是一个抽象方法

抽象类

抽象类:抽象方法所在的类,必须是抽象类才行。在 class 之前写上 abstract 即可

如何使用抽象类和抽象方法: 1. 不能直接创建 new 抽象类对象 2. 必须用一个子类来继承抽象父类 3. 子类必须覆盖重写抽象父类当中所有的抽象方法 覆盖重写(实现): 子类去掉抽象方法 abstract 关键字,然后补上方法体大括号 4. 创建子类对象进行使用

注意事项: 1. 抽象类不能创建对象 2. 抽象类可以有构造方法,供子类创建对象时,初始化父类成员使用。 3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定时抽象类 4. 抽象类的子类,必须重写抽象父类的所有抽象方法,否则,子类也是一个抽象类

接口

接口就是多个类的公共规范,是一种引用数据类型,最重要的内容就是其中的抽象方法

格式类似类,关键字 class 换成关键字 interface ,编译生成的字节码文件仍然是 .java --> .class

1
2
3
puvlic interface 接口名称{
// 接口内容
}

如果是Java 7 1. 常量 [public] [static] [final] 数据类型 常量名称 = 数据值; 2. 抽象方法 [public] [abstract] 返回值类型 方法名称(参数列表);

如果是Java 8 额外添加 3. 默认方法 [public] default 返回值类型 方法名称(参数列表){方法体} 4. 静态方法 [public] static 返回值类型 方法名称(参数列表){方法体}

如果是Java 9 额外添加 5. 私有方法 普通私有方法: private 返回值类型 方法名称(参数列表){方法体} 静态私有方法: private static 返回值类型 方法名称(参数列表){方法体}

上述的带中括号[]的都是默认可以省略不写的,其中default 和之间在18中提到的(defualt)不同,不是留空,是要真正写出来的!

接口使用的步骤: 1. 接口不能直接使用,必须有一个“实现类” 来 “实现” 该接口 2. 接口的实现类 implements 接口的类 ,必须覆盖重写(实现)接口中所有的抽象方法。 3. 创建实现类的对象,进行使用

接口中的抽象方法

  • 接口当中的抽象方法,修饰符必须是两个固定的关键字:public abstract,可以选择性的省略,方法的三要素(名称,参数,返回值)可以随意
  • 接口的实现类(implements接口的类),必须覆盖重写(实现)接口中所有的抽象方法。如果实现类没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类!

接口中的默认方法

  • 接口当中的默认方法,可以解决接口升级的问题(接口在投入使用后,又要在接口中加入新的方法)固定关键字public default 其中public可以省略
  • 接口的默认方法,可以通过接口实现类对象直接调用
  • 接口的默认方法,也可以被接口的实现类进行覆盖重写

接口中的静态方法

  • 不能通过接口实现类的对象来调用接口当中的静态方法,应该通过接口名称直接调用其中的静态方法

上述记录于2019.10.30

接口中的私有方法

  1. 普通私有方法,解决多个默认方法之间重复代码问题
  2. 静态私有方法,解决多个静态方法之间重复代码问题

接口中的常量(成员变量)

  1. 必须使用public static final修饰(可以省略不写)
  2. 必须赋值(没有默认值),赋值后不可修改
  3. 常量用完全大写字母,用下划线分隔

注意事项: 1. 接口不能有静态代码块和构造方法 2. 一个类的直接父类是唯一的,但是一个类可以同时实现多个接口 3. 如果实现类所实现的多个接口当中,存在重名的抽象方法,那么只需要覆盖重写一次即可。 4. 如果实现类所实现的多个接口当中,存在重名的默认方法,那么实现类要对重名的默认方法覆盖重写! 5. 如果实现类没有覆盖重写接口中所有的抽象方法,那么这个实现类自己就必须是抽象类! 6. 一个类如果直接父类当中的方法,和接口当中的默认方法产生了冲突(重名),优先用父类的默认方法

继承与实现: 1. 类与类之间是单继承的,直接父类只有一个 2. 类与接口之间是多实现的,一个类可以实现多个接口 3. 接口与接口之间是多继承的 4. 多个父接口中的抽象方法重名,没关系,子类只需要覆盖重写一个即可。 5. 多个父接口中的默认方法重名,子接口必须默认重写默认方法,而且带着 default

多态 (左父右子)

extends继承或者implements实现,是多态的前提。 代码当中体现多态性,其实就是一句话:父类引用指向子类对象 格式: 父类名称 对象名 = new 子类名称(); 或者 接口名称 对象名 = new 实现类名称(); 具体规则参见继承章节!

对象的向上转型

其实就是多态写法 父类名称 对象名 = new 子类名称(); Animal animal = new Cat(); 右侧创建一个子类对象,把它当作父类来看待使用 类似于自动类型转换: double num = 100; int --> double 向上转型一定是安全的从小范围转向了大范围 弊端: 无法调用子类原本特有的内容!因为无论是调用子类的成员方法还是成员变量,根据多态口诀,编译都要先看左(参见继承),而左边的父类没有子类特有的内容,无法调用,编译就会报错!

对象的向下转型

其实是一个还原的动作 子类名称 对象名 = (子类名称)父类对象; 将父类对象,还原成为本来的子类对象。

1
2
3
4
// 注意,只能还原成本来的子类,不能还原成父类的其他的子类
Animal animal = new Cat();
Cat cat = (Cat) animal;
Dog dog = (Dog) animal; //编译通过,但是运行发生类转换异常ClassCastException java.lang.ClassCastException

必须保证对象本来是什么,就向下转型成什么,不能转型成别的!编译会通过,但是运行会出现异常 java.lang.ClassCastException 类似于强制类型转换: int num = (int) 10.0;// 可以 int num = (int) 10.5; //不可以 精度损失

instanceof关键字

对象 instanceof 类名称 返回值 boolean,判断前面的对象能不能当作后面类型的实例

上述记录与2019.10.31

final关键字

  • 用来修饰一个类
1
2
3
public final class 类名称 {
// ...
}

当前这个类不能有任何子类。(太监类) 但是一定有父类(Object) 其中的所有成员方法,都无法进行覆盖重写,但可以对其父类的方法做覆盖重写。

  • 修饰一个方法 这个方法就是最终方法(不能被覆盖重写)
1
2
3
修饰符 final 返回值类型 方法名称(参数列表) {
// 方法体
}

对于类和方法来说,abstract关键字(一定要覆盖重写)和final关键字(不能覆盖重写)不能同时使用,产生矛盾

  • 修饰一个局部变量 一旦使用 final 来修饰局部变量,这个变量就不能进行更改,并且必须赋予初始值。 一次赋值,终生不变 对于基本类型来说,不可变说的是变量当中的数值不可改变 对于引用类型来说,不可变说的是变量当中的地址值不可改变,但是地址对应的内容可以改变

  • 修饰一个成员变量 这个变量照样不可变 由于成员变量具有默认值,所以用了 final 之后,必须手动赋值,不会再给默认值了。 要么使用直接赋值,要么通过构造方法赋值。二者选其一。 当选构造方法赋值时:必须保证类当中所有重载的构造方法,都最终会对 final 修饰的成员变量赋值

权限修饰符

四种权限修饰符: public > protected > (default) > private 同一个类 yes yes yes yes 同一个包 yes yes yes no 不同包子类 yes yes no no 不同包非子类 yes no no no

内部类

一个类内部包含另一个类 内部类与外部类并不是继承关系 例如人体和心脏,汽车和发动机的关系

分类: 1. 成员内部类 2. 局部内部类(包含匿名内部类)

成员内部类

定义格式:

1
2
3
4
5
6
修饰符 class 外部类名称 {
修饰符 class 内部类名称 {
// ...
}
// ...
}

内用外,可以随意访问,不受权限修饰符影响 外用内,需要借助内部类对象。

成员内部类文件格式: 编译之前,内部类是写在外部类的.java文件中,编译之后与外部类分开,单独生成一个外部类名称$内部类名称.class文件,这也是为什么之前标识符提到不推荐用$命名类,就是为了这里避免混淆。

使用内部类的两种方式: 1. 间接方式:在外部类的方法当中使用内部类;然后通过 main 调用外部类的方法,从而使用了内部类。 2. 直接方式:外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();

如果出现了重名现象,那么访问外部类成员变量的格式: 外部类名称.this.外部类成员变量

1
2
3
4
5
6
7
8
9
10
11
12
public class Outer {
int num = 10; // 外部类的成员变量
public class Inner { // 成员内部类
int num = 20; // 内部类的成员变量
public void methodInner() { // 内部类的成员方法
int num = 30; // 内部类的成员方法的局部变量
System.out.println(num); // 30 就近原则
System.out.println(this.num); // 20 内部类成员变量
System.out.println(Outer.this.num); //10 外部类成员变量
}
}
}

局部内部类

一个类定义在一个方法内部,只有当前所属的方法才能使用它,出了方法外面就不能用了

定义格式:

1
2
3
4
5
6
7
修饰符 class 外部类名称 {
修饰符 返回值类型 外部类方法名称(参数列表) {
class 局部类名称 {
// ...
}
}
}

根据类选择权限修饰符规则: 1. 外部类:public / (default) 2. 成员内部类:public / protected / (default) / private 3. 局部内部类:什么都不能写 4. 如果希望局部内部类访问所在方法的局部变量,那么这个局部变量必须是 final 的。从Java 8 开始,只要局部变量实时不变,那么 final 关键字可以省略。

匿名内部类

如果接口的实现类(或者是父类的子类) 只需要使用唯一的一次 那么这种情况下可以省略掉该类的定义,改为使用匿名内部类 定义格式:

1
2
3
4
5
// 注意右边,这里不是在new接口,接口是不能直接new对象的,这里是省略的实现接口的类的名字!其实就是匿名类是在创建对象的同时进行了定义,因此只能用一次。
// 左边是一个多态!有对象名和赋值符 所以不是匿名对象!
接口名称 对象名 = new 接口名称() {
// 覆盖重写所有抽象方法
}

对格式进行解析 new 接口名称() {...} 1. new 代表创建对象的动作 2. 接口名称就是匿名内部类需要实现哪个接口 3. {...} 这才是匿名内部类的内容

匿名类和匿名对象(参见12.10 匿名对象)不是一个东西!

  • 匿名内部类, 在创建对象的时候,只能使用唯一一次,如果希望多次创建对象,而且类的内容一样的话,那么就必须使用单独定义的实现类了。
  • 匿名对象,在调用方法的时候,只能调用唯一一次,如果希望多次调用方法,那么必须给对象取个名字
  • 匿名内部类是省略了实现类/子类名称,但是匿名对象是省略了对象名称
内部类

上述记录于2019.11.1

常用API(二)

Object类

toString方法: 返回该对象的字符串表示。 默认打印对象的地址值 直接打印对象的名字,其实就是调用对象的toString方法 判断一个类的toString方法是否被覆盖重写,直接打印对象System.out.println(该对象),如果得到地址值,则没被重写,否则被覆盖重写。 例如:Random 类的toString 没被重写,ScannerArrayList的被覆盖重写了

equals方法: 指示其他某个对象是否与此对象“相等” 源代码:

1
2
3
public boolean equals(Object obj) {
return (this == obj);
}

其源码里使用的是== 比较运算符 基本数据类型:比较的是值 引用数据类型:比较的是两个对象的地址值

如果要覆盖重写 equals 方法,要注意的是,由于多态Object是无法使用子类的成员变量的。所以我们需要一个向下转型,将Object 转换成想要的子类。

举一个字符串重写了equals方法的例子

1
2
3
4
5
6
7
8
public class Demo01Equals {
public static void main(String[] args) {
String s1 = new String("aa");
String s2 = new String("aa");
System.out.println(s1 == s2); // false == 比较的是地址
System.out.println(s1.equals(s2)); // true equals方法被重写,比较的不是地址,而是内容
}
}

浅拷贝

浅拷贝

Objects类

JDK7以后添加的一个工具类,它提供了一些方法来操作对象,由一些静态方法组成,这些方法是null-save(空指针安全的)或 null-tolerant(容忍空指针的),用域计算对象的hascode 、返回对象的字符串表现形式、比较两个对象。

equals方法: 比较两个对象的时候,Object类的equals方法容易抛出空指针异常,而Objes类中的equals方法就优化了这个问题:

1
2
3
4
5
6
7
8
9
// Objects.equals源码
public static boolean equals(Object a, Object b){
return (a == b) || (a != null && a.equals(b));
}
// 举个例子
String s1 = null;
String s2 = "abc";
System.out.println(s1.equals(s2)); //NullPointerException null不能调用方法。抛出空指针异常!
System.out.println(Objects.equals(s1,s2)); // false 根据源码,会事先判断s1是否是空!不会报错

Date类

java.util.Date类表示特定的瞬间,精确到毫秒 毫秒值的作用:可以对时间和日期进行计算--把字符串格式的日期转换为毫秒进行计算 时间原点:1970 年 1 月 1 日 00:00:00 GMT (英国格林威治时间) 1000毫秒 = 1秒 中国属于东八区,会把时间增加8个小时

DateFormat类

java.text.DateFormat 是日期/时间格式化子类的抽象类 > 日期/时间格式化子类(如 SimpleDateFormat)允许进行格式化(也就是日期 -> 文本)、解析(文本-> 日期)和标准化。

成员方法: - String format(Date date) 将一个 Date 格式化为日期/时间字符串。 - Date parse(String source) 从给定字符串的开始解析文本,以生成一个日期。 由于是抽象类所以使用其子类:SimpleDateFormat 它的有参构造需要传入一个字符串类型构造格式,具体构造格式参见API文档,构造格式中的字母有其具体含义不能更改,但是连接字母的符号可以更改。

上述记录于2019.11.4

Calendar类

是一个抽象类,里面提供了很多操作日历字段的方法(YEAR、MONTH、DAY_OF_MONTH、HOUR等)无法直接创建对象,使用下面的静态方法 Calendar rightNow = Calendar.getInstance();使用默认时区和语言环境获得一个日历。(得到的是子类对象 GregorianCalendar, 通过多态用Calendar接收) 其他常用方法参见API文档

System类

java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作。

1
2
public static long currentTimeMillis(); // 返回以毫秒为单位的当前时间 可以用来测试程序得效率
public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); // 将数组中指定的数据,拷贝到另一个数组中

其他详情方法参见API文档

StringBuilder类

String的底层是一个被final修饰的byte数组,不能改变,所以进行字符串的相加时,内存中就会有多个字符串(也就是多个数组),占用空间多,效率低下! StringBuilder(字符串缓冲区),可以提高字符串的操作效率(看成一个长度可以变化的字符串) 底层也是一个数组,但是没有被final修饰,可以改变长度! StringBuilder在内存中始终是一个数组(初始容量16)如果超出容量,自动扩容,扩大一倍(32) 两个方法:StringBuilder append(); String toString();详情见API文档

包装类

概念

基本数据类型,使用起来非常方便。但是没有对应的方法,来操作这些基本类型的数据,可以使用一个类把基本类型的数据装起来,在类中定义一些方法,这个类叫做包装类,可以使用类中的方法,来操作这些基本类型的数据。

基本类型 对应的包装类(位于java.lang包中)
byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
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
package cn.perdant.day12.demo05;
/*
用Integer举例,演示装箱拆箱过程
*/
public class Demo01Integer {
public static void main(String[] args) {
//装箱1 构造方法
Integer in1 = new Integer(1); // 该方法已经过时
System.out.println(in1); // 1 作为引用类型,输出的不是地址,说明重写了toString方法

Integer in2 = new Integer("1"); //传字符串参数时,要求该字符串可以变成int的才行。比如传"a"就会抛出数字格式化异常 NumberFormatException
System.out.println(in2); // 1

//装箱2 静态方法
Integer in3 = Integer.valueOf(1);
System.out.println(in3); // 1

Integer in4 = Integer.valueOf("1");
System.out.println(in4); // 1

//拆箱
int in5 = in1.intValue();
System.out.println(in5);

}
}

自动拆箱与自动装箱

JDK1.5之后:基本类型的数据和包装类之间可以自动相互转换 包装类无法直接参与运算,但是可以自动转换为基本数据类型,再进行计算 ps:那我他妈的上面白写了???

基本类型与字符串之间的相互转换

基本类型 --> 字符串 1. 基本类型的值+""(空字符串) 最简单方法 2. 包装类的静态方法 toString(参数),不是 Object 类的 toString() ,后者是空参数,所以二者是方法重载的关系 3. String 类的静态方法 valueOf(参数)

字符串 --> 基本类型 包装类的静态方法 parseXXX("数值类型的字符串")

单列集合Collection

集合概述

集合是java中提供的一种容器,可以用来存储多个数据。

集合和数组的区别: 1. 数组的长度是固定的。集合的长度是可变的。 2. 数组中存储的是同一数据类型(基本or引用)的元素;集合存储的都是对象(基本类型自动装箱),而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

Collection集合框架

Collection 集合

两种单列集合:

  • List 接口
  1. 有序的集合(存储和取出元素顺序相同)
  2. 允许存储重复的元素
  3. 有索引,可以使用普通的for循环遍历
  • Set 接口
  1. 不允许存储重复元素
  2. 没有索引(不能使用普通的for循环遍历)
  3. 子类TreeSet是无序集合
  4. 子类HashSet是有序集合

Collection 共性方法 常用功能

java.util.Collection 是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。注意:List接口覆盖重写了此方法!(因为List都有索引,所以重写成了删除索引位置元素)
  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中。

上述记录于2019.11.5

Iterator 接口

迭代器功能介绍

每种集合的存取方式不同,需要一种通用的获取集合的方式----迭代。 Iterator 迭代器是一个接口,无法直接使用,需要使用其实现类,获取实现类的方式比较特殊:Collection 接口中有一个方法,iterator()返回的就是迭代器的实现类对象。

迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
使用步骤:
1. 使用集合中的方法iterator()获取迭代器的实现类对象,使用Iterator接口接收(多态)
2. 使用迭代器中的方法hasNext判断还有没有下一个元素
3. 使用迭代器中的方法next取出集合中的下一个元素

*/
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Demo01Iterator {
public static void main(String[] args) {
Collection<String> coll = new ArrayList<>();
coll.add("伍陆柒");
coll.add("鸡大宝");
coll.add("小飞");
coll.add("梅十三");
coll.add("江主任");
// Iterator<E> 接口的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型
Iterator<String> it = coll.iterator(); // 多态
boolean b = it.hasNext();
System.out.println(b);
String s = it.next(); // true
System.out.println(s); // 伍陆柒
/*
如果集合中没有元素,再调用next取出元素,会抛出NoSuchElementException(没有元素异常)
*/
// 使用while循环
while(it.hasNext()) {
String e = it.next();
System.out.print(" " + e + " "); // 鸡大宝 小飞 梅十三 江主任
}
System.out.println("");
// 使用for循环
for (Iterator<String> it2 =coll.iterator(); it2.hasNext();) {
String e = it2.next();
System.out.print(" " + e + " "); // 伍陆柒 鸡大宝 小飞 梅十三 江主任
}
}
}

增强for循环

1
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.ArrayList;

/*
增强for循环:底层使用的也是迭代器,使用for循环的格式,简化了迭代器的书写
是JDK1.5之后出现的新特性 Collection<E> extends Iterable<E>
Interable<T> 实现这个接口允许对象成为"foreach"语句的目标
*/
public class Demo02Foreach {
public static void main(String[] args) {
demo01();
demo02();
}

//使用增强for循环遍历集合
private static void demo02() {
ArrayList<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add("ddd");
for (String s:list) {
System.out.println(s);
}

}

//使用增强for循环遍历数组
private static void demo01() {
int[] arr = {1,2,3,4,5};
for (int i:arr) {
System.out.println(i);
}
}
}

泛型

概念

是一种未知的数据类型,当我们不知道使用什么数据类型的时候,可以使用泛型 泛型也可以看成是一个变量,用来接收数据类型 Ee: Element 元素 Tt: Type 类型 ArrayList集合在定义的时候:public class ArrayList<E>{...},不知道集合中都会存储什么类型的数据,所以类型使用泛型E 创建对象的时候,就会确定泛型的数据类型 ArrayList<String> list = new ArrayList<>();会把数据类型作为参数传递,赋值给泛型 E 。此时 E 被确定为 String

1
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
package cn.perdant.day13.demo02;

import java.util.ArrayList;
import java.util.Iterator;

public class Demo01Generic {
public static void main(String[] args) {
show01(); // 报错!
show02();


}

/*
创建集合对象, 不使用泛型
好处:
集合默认类型就是Object类型,可以存储任意类型的数据
坏处:
不安全,会引发异常
*/
private static void show01() {
ArrayList list = new ArrayList(); // 没有写<> 不写就是默认Object
list.add("abc");
list.add(1);
// 迭代器遍历
// 获取迭代器 迭代器泛型跟着集合走,集合默认,迭代器也默认Object
Iterator it = list.iterator();
// 使用hasnext 与next
while (it.hasNext()) {
Object obj = it.next();
System.out.println(obj);

// 想要 使用 String类特有方法,length, 但因为是多态,不能使用子类特有方法,所以需要向下转型
// 会抛出ClassCastException 类型转换异常,Integer不能转换为String
String s = (String)obj;
System.out.println(s.length());
}
}

/*
创建集合对象,使用泛型
好处:
1. 避免了类型转换的麻烦
2. 把运行期异常,提升到了编译期
坏处:
泛型是什么类型,就只能存储什么类型的数据
*/
private static void show02() {
ArrayList<String> list = new ArrayList<>();
list.add("abc");
// list.add(1); 编译的时候就会报错
// 使用迭代器遍历
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s + s.length()); // 不需要使用类型转换,直接调用String特有的length方法
}
}
}

定义和使用含有泛型的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.perdant.day13.demo02;

/*
定义一个含有泛型的类,可以模拟ArrayList集合
泛型是一个未知的数据类型,当我们不确定使用什么数据类型的时候,可以使用泛型
泛型可以接收任意的数据类型,可以使用Integer.String.自定义类...
创建对象的时候确定泛型的数据类型
*/
public class GenericClass<E> {
private E name;

public E getName() {
return name;
}

public void setName(E name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.perdant.day13.demo02;

public class Demo02GenericClass {
public static void main(String[] args) {
// 不写泛型,默认Object
GenericClass gc = new GenericClass();
gc.setName("默认Object类,什么数据类型都可以");
Object obj = gc.getName();

// 创建对象,泛型Integer
GenericClass<Integer> gc2 = new GenericClass<>();
gc2.setName(1);

Integer name = gc2.getName();
System.out.println(name);

// 创建对象,泛型String
GenericClass<String> gc3 = new GenericClass<>();
gc3.setName("伍陆柒");
String name1 = gc3.getName();
System.out.println(name1);
}
}

含有泛型的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.perdant.day13.demo02;

/*
定义含有泛型的方法,泛型定义在方法的修饰符和返回值类型之间
在调用方法的时候,确定泛型的数据类型,传递什么类型的参数,泛型就是什么类型
*/
public class GenericMethod {
//定义一个含有泛型的方法
public <M> void method01(M m) {
System.out.println(m);
}

//定义一个含有泛型的静态方法
public static <S> void method02(S s) {
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
package cn.perdant.day13.demo02;

public class Demo03GenericMethod {
public static void main(String[] args) {
// 创建一个GenericMethod对象
GenericMethod gm = new GenericMethod();
/*
调用含有泛型的方法method01
传递什么类型,泛型就是什么类型
*/
gm.method01(10);
gm.method01("abc");
gm.method01(8.8);
gm.method01(true);

// 静态泛型方法
gm.method02("静态方法,不建议创建对象使用");
GenericMethod.method02(2);

}
}

含有泛型的接口

1
2
3
4
5
6
7
8
package cn.perdant.day13.demo02;

/*
定义含有泛型的接口
*/
public interface GenericInterface<I> {
public abstract void method(I i);
}
1
2
3
4
5
6
7
8
9
10
11
12
package cn.perdant.day13.demo02;

/*
* 含有泛型的接口,第一种使用方式:定义接口的实现类,实现接口,指定接口的泛型(迭代器接口就是有泛型的 Scanner就是其实现类)
* */
public class GenericInterfaceImpl1 implements GenericInterface<String> {

@Override
public void method(String s) {
System.out.println(s);
}
}
1
2
3
4
5
6
7
8
9
10
11
package cn.perdant.day13.demo02;

/*
* 含有泛型的接口第二种使用方式:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走
* 就相当于定义了一个含有泛型的类,创建对象的时候确定泛型的类型(例如ArrayList是List的实现类)*/
public class GenericInterfaceImpl2<E> implements GenericInterface<E> {
@Override
public void method(E e) {
System.out.println(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.perdant.day13.demo02;

/*
* 测试含有泛型的接口
* */
public class Demo04GenericInterface {
public static void main(String[] args) {
//创建GenericInterfaceImpl1对象
GenericInterfaceImpl1 gil1 = new GenericInterfaceImpl1();
gil1.method("字符串");

//创建GenericInterfaceImpl2对象
GenericInterfaceImpl2<Integer> gil2 = new GenericInterfaceImpl2<>();
gil2.method(10);

GenericInterfaceImpl2<Double> gil3 = new GenericInterfaceImpl2<>();
System.out.println(8.8);
}
}

泛型通配符

1
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 cn.perdant.day13.demo02;

import java.util.ArrayList;
import java.util.Iterator;

/*
* 泛型的通配符:<?> 代表任意的数据类型
* 不能创建对象使用,只能作为方法的参数使用
* */
public class Demo05Generic {
public static void main(String[] args) {
// ArrayList<?> list = new ArrayList<?>(); 报错,不能在创建对象时使用!
ArrayList<Integer> list01 = new ArrayList<>();
list01.add(1);
list01.add(2);

ArrayList<String> list02 = new ArrayList<>();
list02.add("a");
list02.add("b");
printArray(list01);
printArray(list02);


}

/*
* 定义一个方法,能遍历所有类型的ArrayList集合
* 泛型没有继承的概念!
* */
public static void printArray(ArrayList<?> list) {
Iterator<?> it = list.iterator();
while(it.hasNext()) {
//取出元素是Object,只有它可以接收任意的类型
Object obj = it.next();
System.out.println(obj);
}
}
}

泛型的上限限定: ? extends E 代表使用的泛型只能是E类型的子类/本身 泛型的下限限定: ? super E 代表使用的泛型只能是E类型的父类/本身 平时用的不多

数据结构

本模块只介绍与集合相关的部分,更多内容请另看《数据结构》

LIFO 后进先出

队列 FIFO 先进先出

数组 查询快(数组地址连续,通过首地址找到数组,通过索引快速找到某一个元素) 增删慢(数组长度固定,想要增加或者删除元素,必须创建一个新数组。把原数组复制过来,垃圾回收原数组)

链表 与数组相反: 查询慢(链表地址不连续,每次查询元素,必须从头开始查询) 增删快(链表接口增加/删除元素,对链表的整体结构(指针域和数据域)没有影响,只需要改变指针)

红黑树: 趋近于平衡树(左右叶子节点相等) 查询速度非常快 查询叶子节点最大次数和最小次数不能超过2倍(换句话说 就是趋近于平衡树的不平衡树) 约束规则: 1.节点可以是红色的或者黑色的 2.根节点是黑色的 3.叶子节点(空节点)是黑色的 4.每个红色的节点的子节点都是黑色的 5.任何一个节点到其每一个叶子节点的所有路径上的黑色节点数相同

接口List

有序、有索引(注意索引越界异常)、允许存储重复的元素

ArrayList 本质是一个数组结构,查找快、增删慢。(其中的add方法调用的copyof) 上文使用的ArrayList就实现了List接口,这里就不过多赘述了。 不同步 速度快

LinkedList 本质是链表结构,查询慢,增删快。 里面包含了大量操作首尾元素的特有方法(属于其特有的,不能通过多态调用) 不同步

Vector 所有单列集合的祖宗,底层也是数组 同步 速度慢 了解即可

上述记录于2019.11.6

接口Set

不允许存储重复元素、没有索引 因此没有带索引的方法,不能用普通 for 循环遍历,里面方法和 Collection 方法一致,不过多赘述,毕竟 Collection 是他的父接口啊。。。。

HashSet

java.util.HashSet实现了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
package cn.perdant.day13.demo05;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

public class Demo01Set {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
// 使用add 往集合中添加元素
set.add(1);
set.add(3);
set.add(2);
set.add(1);
// 没有索引,不能用普通for循环 ,用迭代器
Iterator<Integer> it = set.iterator();
while(it.hasNext()) {
Integer n = it.next();
System.out.println(n); // 1 2 3 存储的顺序和取出的顺序可能不一样,而且不存储重复的元素
}

//使用增强for
for (Integer i : set) {
System.out.println(i);
}
}
}

哈希值

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

/*
* 哈希值:是一个十进制的整数,由系统随机给出(就是对象的地址值,是一个逻辑地址,是模拟出来的得到的地址,不是数据实际存储的物理地址,不是 == 运算符比较的那个地址!)
* 在Object类有一个方法,可以获取对象的哈希值
* 方法源码:
* public native int hashCode();
* native:代表该方法调用的是本地操作系统的方法
* */
public class Demo01HashCode {
public static void main(String[] args) {
Person p1 = new Person();
int h1 = p1.hashCode();
System.out.println(h1); // 10进制整数 系统随机给定

Person p2 = new Person();
int h2 = p2.hashCode();
System.out.println(h2);

//toString 方法输出的就是哈希值的十六进制形式
System.out.println(p1); // h1 的16进制形式

// String类重写了Object类的hashCode方法
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1.hashCode()); // 96354 重写了的
System.out.println(s2.hashCode()); // 96354
System.out.println("重地".hashCode()); // 1179395
System.out.println("通话".hashCode()); // 1179395 两个元素不同 但哈希值相同,哈希冲突。
}
}

哈希表

哈希表是 HashSet 的存储结构 JDK1.8之前 哈希表 = 数组+链表 JDK1.8之后 哈希表 = 数组+链表如果链表长度超过8位:哈希表 = 数组+红黑树(提高查询的速度) 数组里存hashCode ,链表/树里存元素

哈希表

Set集合不允许重复元素

Set集合在调用add方法的时候,add会调用元素的hashCode方法和equals方法,判断元素是否重复。hashCode相同时,调用equals看两个元素是否相等(此处的相等不一样的类型有不一样的意思,比如字符串相等是指两个字符串的字符是一样的,所以你自定义的类型相等是要看你自己怎么定义,所以要重写equals)。因此如果要用Set存储自定义类型元素的话,必须重写hashCode方法和equals方法。

HashSet存储自定义类型元素

必须重写HashCode和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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package cn.perdant.day13.demo06;

import java.util.Objects;

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

public Person() {
}

public Person(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 "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
1
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 cn.perdant.day13.demo06;

import java.util.HashSet;
import java.util.Set;

/*
同名和同年龄的人视为同一人,只出现一次!
*/
public class HashSetSavePerson {
public static void main(String[] args) {
HashSet<Person> set = new HashSet<>();
Person p1 = new Person("伍陆柒",25);
Person p2 = new Person("伍陆柒",25);
Person p3 = new Person("伍陆柒",24);
System.out.println(p1.hashCode()); // 640675041
System.out.println(p2.hashCode()); //640675041 重写hashCode 返回的哈希值相等

System.out.println(p1 == p2); // false 地址不同 是两个对象
System.out.println(p1.equals(p2)); // true 重写equals 同名同年龄返回true
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println(set); // [Person{name='伍陆柒', age=25}, Person{name='伍陆柒', age=24}] 同名同年龄的只出现一次
}

}

LinkedHashSet

HashSet的子类 是有序的! 底层是一个哈希表+链表(用来记录元素的存储顺序,保证元素有序)

可变参数

在JDK1.5以后,如果我们定义一个方法需要接收多个参数,并且多个参数类型一致,我们可以对其简化成如下格式

1
修饰符 返回值类型 方法名(参数类型... 形参名){}

原理: 可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数 传递的参数个数,可以是0个(不传递),1, 2...多个。 传递几个参数,数组的长度就是几。

注意事项: 1. 一个方法的参数列表,只能有一个可变参数 2. 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾

可变参数的终极写法

1
修饰符 返回值类型 方法名(Object... 形参名){}

Collections类

集合工具类,用来对集合进行操作

常用静态方法: 1. addAll一次性添加多个元素、 2. shuffle打乱集合顺序、 3. sort排序(只能传List集合参数,默认升序,对于自定义类,需要实现Comparable接口,覆盖重写compareTo(参数)方法才能使用sort) Comparable接口的排序规则:自己(this)-参数 升序, 反之降序。 4. sort(ListM<T> list, Comparator<? super T>)按照给定规则进行排序 Comparator 是一个接口,里面有compare(o1, o2)方法,需要覆盖重写 Comparator接口的排序规则:o1-o2 升序,反之降序。

1
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 cn.perdant.day13.demo07;

// 实现Comparable接口
public class Person implements Comparable<Person>{
private String name;
private int age;

public Person() {
}

public Person(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 "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

// 覆盖重写compareTo方法
@Override
public int compareTo(Person o) {
return this.getAge() - o.getAge();
}
}
1
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 cn.perdant.day13.demo07;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class Demo01Sort {
public static void main(String[] args) {
ArrayList<Person> list = new ArrayList<>();
Person p1 = new Person("baobo", 18);
Person p2 = new Person("baobo", 20);
Person p3 = new Person("andelu", 18);
Person p4 = new Person("caibi", 15);
Collections.addAll(list, p1, p2, p3, p4); // 初始顺序
System.out.println(list);
//[Person{name='baobo', age=18}, Person{name='baobo', age=20}, Person{name='andelu', age=18}, Person{name='caibi', age=15}]

// sort方法1
Collections.sort(list); // 需要list实现Comparable接口。覆盖重写compareTo方法
System.out.println(list);// 按照年龄升序排序
//[Person{name='caibi', age=15}, Person{name='baobo', age=18}, Person{name='andelu', age=18}, Person{name='baobo', age=20}]

// sort方法2
Collections.sort(list, new Comparator<Person>() { // 使用Comparator的匿名类 构造了一个Comparator的实现类(多态)
@Override
public int compare(Person o1, Person o2) { // 重写compare方法
int result = o1.getAge() - o2.getAge();
if (result == 0) {
result = o1.getName().charAt(0) - o2.getName().charAt(0); // 如果年龄相等,比较名字的首字母大小
}
return result;
}
});
System.out.println(list); // 年龄相等时,按照首字母排序
// [Person{name='caibi', age=15}, Person{name='andelu', age=18}, Person{name='baobo', age=18}, Person{name='baobo', age=20}]
}
}

上述记录于2019.11.7

双列集合Map

Map特点:

  1. 不同于CollectionMap<K,V>集合是一个双列集合,一个元素包含两个值(一个key,一个value)
  2. key和value的数据类型可以相同,也可以不同
  3. key是不允许重复的,value是可以重复的
  4. key和value是一一对应的

HashMap

java.util.HashMap 实现Map接口。 底层和HashSet 一样是哈希表结构(查询速度非常快),无序 不同步(多线程 速度快)

LinkedHashMap

HashMap的子类,底层是哈希表+链表。类似LinkedHashSet,有序(因为加了链表)

LinkedHashMap

Map常用方法

Map接口中定义了很多方法,常用的如下: - public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。若key不存在,返回值为null;否则,返回参数value

key不存在时,返回的是value的默认值,因为value是T类型,也就是引用类型,引用类型的默认值就是null

  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。若key存在,返回key对应的value;否则,返回null
  • public V get(Object key) 根据指定的key键,在Map集合中获取对应的value值。
  • boolean containsKey(Object key) 判断集合中是否包含指定的键。
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(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
package cn.perdant.day14.demo01;

import java.util.HashMap;
import java.util.Map;
/*
* Map集合特点:
* 1. Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
* 2. key和value的数据类型可以相同,也可以不同
* 3. key是不允许重复的,value是可以重复的
* 4. key和value是一一对应的*/
public class Demo01Map {
public static void main(String[] args) {
show01();
}
private static void show01() {
// 创建Map集合对象 多态
Map<String, String> map = new HashMap<>();
String v1 = map.put("伍陆柒", "可乐");
System.out.println("v1: " + v1); // null key不重复 返回空
String v2 = map.put("伍陆柒", "梅十三");
System.out.println("v1: " + v2); // 可乐 key重复 返回被替换的值
System.out.println(map);
map.put("阿珍","阿强");
map.put("汪峰","咪咪");
map.put("喵财","咪咪");
System.out.println(map); // {伍陆柒=梅十三, 喵财=咪咪, 汪峰=咪咪, 阿珍=阿强} 无序,不一定按照输入的顺序输出
// key 不能重复,但是value可以重复
}
}
1
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 cn.perdant.day14.demo01;

import java.util.HashMap;
import java.util.Map;

public class Demo02Map {
public static void main(String[] args) {
show02();
}
public static void show02(){
Map<String, Integer> map = new HashMap<>();
map.put("可乐",165);
map.put("江主任",178);
map.put("梅十三",168);
System.out.println(map); // {可乐=165, 梅十三=168, 江主任=178}
Integer v1 = map.remove("江主任");
System.out.println("v1: " + v1); // 178 删除的key对应的value
System.out.println(map); // {可乐=165, 梅十三=168}
Integer v2 = map.remove("伍陆柒"); // 使用包装类接收,因为有可能是null 基本数据类型int没有null
System.out.println("v2" + v2); // null 没有找到此key
System.out.println(map); // {可乐=165, 梅十三=168}

Integer v3 = map.get("可乐");
Integer v4 = map.get("伍陆柒");
System.out.println("v3 和 v4 分别是: " + v3 + " " + v4); // 165 null
boolean b1 = map.containsKey("可乐");
boolean b2 = map.containsKey("伍陆柒");
System.out.println("b1 和 b2 分别是: " + b1 + " " + b2); // true false
}
}

遍历Map集合

遍历方法: 1. keySet : 把Map集合中所有的key取出来存储到Set集合中,这样就可以通过遍历Set遍历key,通过get方法遍历value。 2. entrySet: 把Map集合内部得多个Entry对象取出来,存储到一个Set集合中,遍历Set集合,获取Set集合中的每一个Entry对象。再使用Entry对象中的方法getKeygetValue方法。

1
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 cn.perdant.demo02;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class MapIteration {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("老板", "绯红之王");
map.put("吉良吉影", "杀手皇后");
map.put("DIO", "The World!");
map.put("JOJO", "Gold Experience");

// keySet方法遍历
Set<String> set = map.keySet();
for (String key : set) {
String value = map.get(key);
// key与value之间的链接符可以随意更改
System.out.println(key + "--" + value);
}

/*老板--绯红之王
JOJO--Gold Experience
吉良吉影--杀手皇后
DIO--The World!*/

// entrySet方法遍历
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry<String, String> entry : entries) {
// key与value作为一个整体输出,之间的链接符默认为=
System.out.println(entry);
// 也可以分别用Entry对象的方法 输出key和value,这样链接符可以随意更改
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+"=" +value);
}

/*老板=绯红之王
JOJO=Gold Experience
吉良吉影=杀手皇后
DIO=The World!*/
}
}

Entry键值对对象

EntryMap 的嵌套类(内部类) 作用:当Map集合一创建,就会在Map集合中创建一个Entry对象,用来记录键与值(键值对对象,键与值的关系)

HashMap存储自定义类型键值

类似 31.5 HashSet存储自定义类型元素 为了保证key值唯一,作为key元素的类型,必须覆盖重写hashCode方法和equals方法!

LinkedHashMap

继承了HashMap集合 底层原理:哈希表+链表结构(记录元素的顺序)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.perdant.day14.demo02;

import java.util.HashMap;
import java.util.LinkedHashMap;

public class Demo01LinkedHashMap {
public static void main(String[] args) {
HashMap<String, String> map = new HashMap<>();
map.put("a", "a");
map.put("c", "c");
map.put("b", "b");
map.put("a", "d");
System.out.println(map); // {a=d, b=b, c=c} 不允许重复,无序
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("a", "a");
linkedHashMap.put("c", "c");
linkedHashMap.put("b", "b");
linkedHashMap.put("a", "d");
System.out.println(linkedHashMap); // {a=d, c=c, b=b} 有序 同样不允许重复
}
}

HashTable

JDK1.0就有了,最早期的集合 底层也是哈希表 键与值都不允许存储null(之前学的所有集合都可以),同步 线程安全 单线程,速度慢 和Vector一样,都被1.2版本之后的HashMap取代了 但是其子类Properties依然活跃 ,Properties是唯一一个和IO流相结合的集合

JDK9优化

Set Map List 三个接口中都定义了新的方法 of

1
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 cn.perdant.day14.demo04;

import java.util.List;
import java.util.Map;
import java.util.Set;

/*
* JDK9的新特性:
* List、Set、Map接口里面增加了一个静态的方法of,可以一次性添加多个元素
* 前提:
* 当集合中存储的元素的个数已经确定了,不再改变时使用
* 注意:
* 1. of方法只适用于List接口、Set接口、Map接口,不适用于接口的实现类
* 2. of方法的返回值是一个不能改变的集合,集合不能再使用add或者是put方法添加元素
* 3. Set和Map接口在调用of方法的时候,不能有重复的元素,否则会抛出异常*/
public class Demo01JDK {
public static void main(String[] args) {
List<String> list = List.of("a", "b", "a", "c", "d");
System.out.println(list); // [a, b, a, c, d]
// list.add("w"); 报错 UnsupportedOperationException 不支持操作异常
// Set<String> set = Set.of("a", "b", "a", "c", "d"); 报错 有重复的参数 IllegalArgumentException 非法参数异常
Set<String> set = Set.of("a", "b", "c", "d");
System.out.println(set); // [a, c, b, d]

Map<String, Integer> map = Map.of("伍陆柒", 25, "梅十三", 23, "鸡大宝", 28);// 也不能重复
System.out.println(map); // {梅十三=23, 鸡大宝=28, 伍陆柒=25}

}
}
集合类型
集合框架
集合框架中的类

Debug追踪

可以让代码逐行执行,查看代码的过程,调式程序中出现的bug

使用方式: 1. 在行号的右边,鼠标左键单击,添加断点(初学时添加在每个方法的第一行,熟练了之后 哪里有bug添加到哪里) 2. 右键,选择Debug执行程序 3. 程序就会停留在添加的第一个断点处

执行程序: f8:逐行执行程序 f7:进入到方法中 shift+f8:跳出方法 f9:跳到下一个断点,如果没有下一个断点,那么就结束程序 ctrl+f2:退出debug模式,停止程序 Console:切换到控制台

上述记录于2019.11.8

异常

异常的概念

异常指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止。 在java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出一个异常对象。Java处理异常的方式是中断处理。 异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行!

异常的体系

异常的根类java.lang.Throwable 其下有两个子类:java.lang.Errorjava.lang.Exception

  • Error错误:错误就相当于程序得了一个无法治愈的病,必须修改源代码,程序才能继续执行。
  • Exception编译期异常,进行编译(写代码)时Java程序出现的问题。这种异常在写代码的时候编译器就会指出来。例如:java.io.IOException
    • 其子类 RuntimeException:运行期异常,java程序运行过程中出现的问题。这种异常在写代码的时候编译器不会指出来,但是如果不处理的话,编译运行的时候会产生异常对象,并且默认向上抛出,直到抛给JVM,Java虚拟机的处理方式是:打印异常信息、终止当前正在执行的Java程序。(看不懂这段话没关系,看完下面的图,再回来看就理解了。)
    • 异常就相当于程序得了一个小毛病,把异常处理掉,程序可以继续执行。
  • 简而言之:编译期异常必须处理,使用try...catch或throws;运行期异常可不处理,默认交给JVM处理

异常产生过程的解析

分析异常是怎么产生的,如何处理异常

异常的产生过程解析

异常的处理

五个关键字try catch finally throw throws

抛出异常 throw

1
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 cn.perdant.day15.demo01;
/*
* throw 关键字
* 作用:
* 可以使用throw关键字在指定的方法中抛出指定的异常
* 使用格式:
* throw new xxxException("异常产生的原因")
* 注意:
* 1. throw关键字必须写在方法的内部
* 2. throw关键字后边new的对象必须是Exception或者Exception的子类对象
* 3. throw关键字抛出指定的异常对象,我们就必须处理这个异常对象
* throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM处理(打印异常对象,中断程序)
* throw关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try...catch
*/
public class Demo03Throw {
public static void main(String[] args) {
/* int[] arr = null;
int e = getElement(arr, 0);
System.out.println(e);*/ // Exception in thread "main" java.lang.NullPointerException: 传递的数组值为null
int[] arr1 = new int[3];
int e1 = getElement(arr1, 3); // Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 数组的索引越界了
System.out.println(e1);
}
/*
* 以后工作中,必须对方法传递过来的参数进行合法性校验
* 如果参数不合法,那么就必须使用抛出异常的方式,告知方法的调用者,传递的参数有问题
*/
public static int getElement(int[] arr, int index) {
if (arr == null) {
throw new NullPointerException("传递的数组值为null"); // NullPointerException是一个运行期异常,我们不用处理,默认交给JVM处理
}
if (index < 0 || index > arr.length-1) {
throw new ArrayIndexOutOfBoundsException("数组的索引越界了"); // ArrayIndexOutOfBoundsException也是运行期异常
}
int ele = arr[index];
return ele;
}
}

Objects非空判断

之前提到过Objects类提供一些静态方法,这些方法是空指针安全的或容忍空指针的,因为它对对象为null的值进行了抛出异常操作。 public static <T> T requireNonNull(T obj): 查看指定引用对象是不是null

1
2
3
4
5
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}

throws关键字 异常处理的第一种方式

1
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 cn.perdant.day15.demo01;

import java.io.FileNotFoundException;
import java.io.IOException;

/*
* throws关键字:异常处理的第一种方式,交给别人处理
* 作用:
* 当方法内部抛出异常对象的时候,那么我们必须处理这个异常对象
* 可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人处理),最终交给JVM处理 --> 中断处理
* 使用格式:在方法声明时使用
* 修饰符 返回值类型 方法名(参数列表) throws AAAException, BBBException...{
* throw new AAAException("异常产生的原因");
* throw new BBBException("异常产生的原因");
* ...
* }
* 注意:
* 1. throws关键字必须写在方法声明处
* 2. throws关键字后面声明的异常必须是Exception或者是Exception的子类
* 3. 方法内部如果抛出了多个异常对象,那么throws后边必须也声明多个异常
* 如果抛出的多个异常对象有子父类关系,那么直接声明父类异常即可
* 4. 调用了一个声明抛出异常的方法,我们就必须处理声明的异常
* 要么继续使用throws声明抛出,交给方法的调用者处理,最终交给JVM
* 要么try... catch自己处理异常
*/
public class Demo05Throws {
public static void main(String[] args) throws IOException { // 继续声明,交给JVM处理
readFile("c:\\a.pdf"); // Exception in thread "main" java.io.IOException: 文件的后缀名不对 JVM 处理一个异常便会终止程序,不会处理后续代码了。但是try catch可以!
}
public static void readFile(String fileName) throws IOException { // FileNotFoundException 是 IOException 的子类,只抛出父类即可
if (!fileName.endsWith(".txt")) {
throw new IOException("文件的后缀名不对");
}

if (!fileName.equals("c:\\a.txt")) {
throw new FileNotFoundException("文件路径不是c:\\a.txt"); // FileNotFoundException 是编译异常,抛出了编译异常,就必须处理这个异常,可以使用throws 继续声明抛出FileNotFoundException 这个异常对象,让方法的调用者处理
}

System.out.println("路径没有问题");
}
}

try... catch关键字 异常处理的第二种方式

各人见解: try..catchthrows 好的地方在于,throws 如果一直向上抛出,抛给虚拟机之后,虚拟机会直接终止程序运行,而有时候我们遇到异常,并不希望整个程序停止,利用try...catch我们可以合理的控制异常的大小,什么样的异常需要终止程序,什么样的异常可以忽略,什么样的异常可以做一些特殊的操作。比throws灵活很多。

1
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 cn.perdant.day15.demo01;

import java.io.IOException;

/*
* try...catch: 异常处理的第二种方式,自己处理异常
* 格式:
* try {
* 可能产生异常的代码
* }catch(定义一个异常的变量,用来接收try中抛出的异常对象){
* 异常的处理逻辑,产生异常对象之后,怎么处理异常对象
* 一般在工作中,会把异常的信息记录到一个日志中
* }
* ...
* catch(异常类名 变量名){
* catch可以有多个
* }
* 注意:
* 1. try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
* 2. 如果try中产生了异常,那么就会执行catch中的异常处理逻辑,执行完毕catch中的处理逻辑,继续执行try...catch之后的代码
* 如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try...catch之后的代码
*/
public class Demo01TryCatch {
public static void main(String[] args) {
try{
readFile("d:\\a.tx");
}catch (IOException e) { // try 中抛出什么异常对象,catch就定义什么异常变量,用来接收这个异常对象
// 异常处理逻辑
System.out.println("catch --> 传递的文件后缀不是.txt");
System.out.println(e.getMessage()); // 文件的后缀名不对
System.out.println(e.toString()); // java.io.IOException: 文件的后缀名不对
System.out.println(e); // 同上
e.printStackTrace();
/*java.io.IOException: 文件的后缀名不对
at cn.perdant.day15.demo01.Demo01TryCatch.readFile(Demo01TryCatch.java:39)
at cn.perdant.day15.demo01.Demo01TryCatch.main(Demo01TryCatch.java:25)*/
}
System.out.println("后续代码"); // 后续代码 无论是否发生异常都不影响这句话的执行
}

public static void readFile(String fileName) throws IOException {
if (!fileName.endsWith(".txt")) {
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题");
}
}

上述记录于2019.11.12

上面的代码用到了Throwable类中定义的3个 异常处理的方法:

String getMessage() 返回 throwable 的简短信息

String toString() 返回 throwable 的详细信息

void printStackTrace() JVM打印异常对象,默认此方法,打印的异常信息是最全面的

finally代码块

对于try 块里面的代码,发生异常后 ,后面的代码是执行不到的。我们可以把这些代码放到finally当中。

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

import java.io.IOException;

/*
* finally代码块
* 格式:
* try {
* 可能产生异常的代码
* }catch(定义一个异常的变量,用来接收try中抛出的异常对象){
* 异常的处理逻辑,产生异常对象之后,怎么处理异常对象
* 一般在工作中,会把异常的信息记录到一个日志中
* }
* ...
* catch(异常类名 变量名){
* catch可以有多个
* }finally{
* 无论是否出现异常,都会执行。
* }
* 注意:
* 1. finally不能单独使用,必须和try一起使用
* 2. finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放(IO)
*/
public class Demo06Finally {
public static void main(String[] args) {
try {
//可能会产生异常的代码
readFile("c:\\a.tx");
} catch (IOException e) {
//异常的处理逻辑
e.printStackTrace();
} finally {
// 无论是否出现异常,都会执行
System.out.println("资源释放");
}
}
public static void readFile(String fileName) throws IOException {
if (!fileName.endsWith(".txt")) {
throw new IOException("文件的后缀名不对");
}
System.out.println("路径没有问题");
}
}

各人见解: 之前说过,try..catch的一个好处在于,无论是否发生异常,都不影响后面代码的执行,那么写不写finally的区别在哪呢??我自己试了一下后,才理解了这一节一开始的那一句话:

对于try 块里面的代码,发生异常后 ,后面的代码是执行不到的。我们可以把这些代码放到finally当中。

先看一个例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo01Exception {
public static void main(String[] args) {
int e = demo();
System.out.println(e); // finally trumps return. 0
}

public static int demo() {
try {
return 0;
}
finally {
System.out.println("finally trumps return.");
}
}
}

在上面的代码运行时,实际执行顺序是在return之前执行了finally中的输出语句。 如果不用finally只是单单写在try...catch后面的语句是不会执行的,因为在try里已经return了,这就是所说的无论如何,finally里的代码一定会执行。 但是,还有两点需要注意,看下面的例子。 第一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo01Exception {
public static void main(String[] args) {
int e = demo();
System.out.println(e); // 12 2
}

public static int demo() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 12;
System.out.println(i);
}
}
}

如果按照前面所说的逻辑,为什么不是输出12 12呢?这里涉及到了JVM的深层原理,我还没学到。。。大体意思就是,return里面的值和变量i里面的值 是分开存的,所以finally里面虽然修改了i,但是return里面的值没有变,所以最后返回的仍是2。 第二点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Demo01Exception {
public static void main(String[] args) {
int e = demo();
System.out.println(e); // finally trumps return. 12
}

public static int demo() {
int i = 0;
try {
return i;
} catch (Exception e) {
i = 1;
return i;
} finally {
i = 12;
System.out.println("finally trumps return.");
return i;
}
}
}

这就是为什么finally块中不要有return,如果有的话,程序不论是否发生异常,都会执行finally中的语句,最后必定返回的是finally 中的结果。

异常的注意事项

1
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
package cn.perdant.day15.demo01;

import java.util.List;

public class Demo03Exception {
public static void main(String[] args) {
/*
* 多个异常使用捕获又该如何处理呢?
* 1. 多个异常分别处理。
* 2. 多个异常一次捕获,多次处理。
* 3. 多个异常一次捕获一次处理*/

// 1. 多个异常分别处理
// try {
//
// int[] arr = {1,2,3};
// System.out.println(arr[3]); // ArrayIndexOutOfBoundsException: 3
// }catch (ArrayIndexOutOfBoundsException e) {
// System.out.println(e);
// }
// try{
//
// List<Integer> list = List.of(1, 2, 3);
// System.out.println(list.get(3)); // IndexOutOfBoundsException: Index 3 out-of-bounds for length 3
// } catch (IndexOutOfBoundsException e) {
// System.out.println(e);
// }

//2. 多个异常,一次捕获,多次处理
// try {
//
// int[] arr = {1,2,3};
// System.out.println(arr[3]); // ArrayIndexOutOfBoundsException: 3
// List<Integer> list = List.of(1, 2, 3);
// System.out.println(list.get(3)); // IndexOutOfBoundsException: Index 3 out-of-bounds for length 3
// }catch (ArrayIndexOutOfBoundsException e) {
// System.out.println(e);
// } catch (IndexOutOfBoundsException e) {
// System.out.println(e);
// }
/*
* 一个try 多个catch 注意事项:
* catch里面定义的一场变量,如果有子父类关系,那么子类的异常变量,必须写在上边,否则就会报错
*/
/*try {

int[] arr = {1,2,3};
System.out.println(arr[3]);
List<Integer> list = List.of(1, 2, 3);
System.out.println(list.get(3));
}catch (IndexOutOfBoundsException e) {
System.out.println(e);
} catch (ArrayIndexOutOfBoundsException e) { // 报错, ArrayIndexOutOfBoundsException 是 IndexOutOfBoundsException 的子类,必须写在上边
System.out.println(e);
}*/

//3. 多个异常,一次捕获一次处理
// try {
//
// int[] arr = {1,2,3};
// System.out.println(arr[3]);
// List<Integer> list = List.of(1, 2, 3);
// System.out.println(list.get(3));
// }catch (Exception e) {
// System.out.println(e);
// }

// 运行期异常被抛出可以不处理,即不捕获也不声明抛出。
// 默认给虚拟机处理,终止程序,什么时候不抛出运行期异常了,再来执行程序
int[] arr = {1,2,3};
System.out.println(arr[3]); // ArrayIndexOutOfBoundsException: 3
List<Integer> list = List.of(1, 2, 3);
System.out.println(list.get(3));

System.out.println("后续代码");
}
}
  • 如果finally有return语句,永远返回finally中的结果,避免该情况!(finally中是一定会执行的代码)
  • 如果父类抛出多个异常,子类重写父类方法时,抛出和父类相同的异常或者是抛出父类抛出的异常的子类或者不抛出异常。
  • 父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出,简而言之:父类异常是什么样,子类异常就是什么样
  • try...catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package cn.perdant.day15.demo01;

public class Fu {
public void show01() throws NullPointerException,ClassCastException{}
public void show02() throws IndexOutOfBoundsException{}
public void show03() throws NullPointerException,ClassCastException{}
public void show04() {}


}
class Zi extends Fu{
// 抛出和父类相同的异常
@Override
public void show01() throws NullPointerException,ClassCastException{}

// 父类异常的子类
@Override
public void show02() throws ArrayIndexOutOfBoundsException{}

// 不抛出异常
@Override
public void show03() {}

//父类方法没有抛出异常,子类重写父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出
@Override
public void show04() {
try{
throw new IndexOutOfBoundsException("编译期异常");
} catch (Exception e) {
System.out.println(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
package cn.perdant.day15.demo01;
/*
* 自定义异常类:
* java提供的异常类,不够我们使用,需要自己定义一些异常类
* 格式:
* public class XXXException extends Exception | RuntimeException{
* 添加一个空参数的构造方法
* 添加一个带异常信息的构造方法
* }
* 注意:
* 1. 自定义异常类命名一般都是以Exception结尾,说明该类是一个异常类
* 2. 自定义异常类,必须的继承Exception或者RuntimeException
* 继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try...catch
* 继承RuntimeException:那么自定义的异常类就是一个运行期异常,可以不处理,交给虚拟机处理(中断处理)
*/
public class RegisterException extends Exception{
// 添加一个空参数的构造方法
public RegisterException(){
super();
}
// 添加一个带异常信息的构造方法 查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部回调用父类带异常信息的构造方法,让父类来处理这个异常信息
public RegisterException(String message){
super(message);
}
}

多线程

并发与并行

并发:指两个或多个事件在同一个时间段内发生 并行:指两个或多个事件在同一时刻发生(同时发生)

并发与并行

线程和进程

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
  • 线程: 是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。简而言之:一个程序运行后至少有一个进程,一个进程中可以包含多个线程

进程:

进程的概念

线程: 线程概念

线程的调度 分时调度:轮流使用cpu 抢占式调度:谁的优先级高,谁来抢占cpu (java使用的此种调度)

各人思考: 一个程序有多个进程,一个进程有多个线程, 线程和cpu是什么关系?网上找了些资料,先说结论:多线程和cpu有几个核没关系,单核也可以开多线程,cpu在多个线程间来回切换,多核开多线程的优势在于速度更快罢了。

下面放上我在网上找的摘录: >1. CPU和线程的关系 >(1)第一阶段,单CPU时代,单CPU在同一时间点,只能执行单一线程。比如,的某一刻00:00:00 这一秒,只计算1+1=2(假设cpu每秒计算一次) >(2)第二阶段,单CPU多任务阶段,计算机在同一时间点,并行执行多个线程。但这并非真正意义上的同时执行,而是多个任务共享一个CPU,操作系统协调CPU在某个时间点,执行某个线程,因为CPU在线程之间切换比较快,给人的感觉,就好像多个任务在同时运行。比如,电脑开了两个程序qq和qq音乐,假设这两个程序都只有一个线程。人能够感觉到CPU切换的频率是一秒一次,假设当前cpu计算速度是1秒1次,那么我们就能明显感到卡顿,当聊天,点击发送按钮时候,qq音乐就会停止运行。当前cpu计算速度是1秒100次,也就是它能在一秒之内在这两个进程见切换100次,那么我们就感不到卡顿,觉得QQ和QQ音乐是同时在运行。 >(3)第三阶段,多CPU多任务阶段,真正实现的,在同一时间点运行多个线程。具体到哪个线程在哪个CPU执行,这就跟操作系统和CPU本身的设计有关了。 >1. 举例说明 >(1)假设一种极端情况,一台单核计算机,只运行2个程序A和B。 >假设A和B的优先级相同,A有3个线程,B有1个线程,那么CPU分配给A和B的执行时间应该是3:1。 >(2)假设同一种情况发生在一台多核计算机,核1处理A和B各一个线程,核2处理A剩下的线程。 >(3)刚才说的是线程只消耗CPU,在实际应用中这种情况是不存在的,程序总会跟资源打交道,比如读个文件,查询数据库,访问网络,这个时候多线程才能体现出优势。在一个进程中,让A先用一下CPU去查询数据库,在A查询数据库的时候CPU空闲,B就用一CPU去读文件,让C去访问网络。相对于查询数据库,读取文件这些操作来说,CPU的计算时间几乎可以忽略不计。所以,多线程,实际上是计算机多种资源的并行运用,跟CPU有几个核心没什么关系。

主线程:

主线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.perdant.day15.demo02;

/*
* 主线程:执行主(main)方法的线程
* 单线程程序:java程序中只有一个线程
* 程序从main方法开始,从上到下依次执行
* JVM执行main方法,main方法会进入到栈内存
* JVM会找操作系统开辟一条main方法通向cpu的执行路径
* cpu就可以通过这个路径来执行main方法
* 而这个路径有一个名字,叫main(主)线程
*/
public class Demo01MainThread {
public static void main(String[] args) {
Person p1 = new Person("伍陆柒");
p1.run();
System.out.println(0/0); // Exception in thread "main" java.lang.ArithmeticException: / by zero
Person p2 = new Person("梅十三");
p2.run();
}
}

方式一:创建线程类

Java使用java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(即一段顺序执行的代码)。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
package cn.perdant.day15.demo03;
/*
* 创建多线程程序的第一种方式:创建Thread类的子类
* java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
* 实现步骤:
* 1. 创建一个Thread类的子类
* 2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
* 3. 创建Thread类的子类对象
* 4. 调用Thread类中的方法start方法,开启新的线程,执行run方法。
* 此时就有两个线程,main线程和创建的新线程,两个并发运行
* 多次启动一个线程是非法的,当线程已经结束执行后,不能再重新启动。
* java属于抢占式调度,哪个线程优先级高,哪个线程优先执行,优先级相等时,随机选择一个执行。
*/
public class Demo01Thread {
public static void main(String[] args) {
// 3.创建Thread类的子类对象
MyThread mt = new MyThread();
// 4.调用start方法
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main:" + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
// 1. 创建一个Thread类的子类
public class MyThread extends Thread {
// 2.重写run 设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run:" + i);
}
}
}

多线程原理:

多线程打印结果

多线程内存图解: 多线程内存图解

上述记录于2019.11.13

Thread类

常用方法

currentThread() getName() setName() sleep()

1
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 cn.perdant.day15.demo03;

/*
* 获取线程的名称
* 1. 使用Thread类中的方法getName()
* 2. 可以先获取到当前正在执行的线程currentThread,再使用线程中的方法getName()获取线程的名称
* 设置线程的名称
* 1. 使用Thread类中的方法setName(名字)
* 2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字。
*/
public class MyThread extends Thread {

public MyThread(){
}
public MyThread(String name) {
super(name);
}
//重写run 设置线程任务
@Override
public void run() {
/* String name = getName();
System.out.println(name);*/
/* Thread t = Thread.currentThread();
// System.out.println(t); // Thread[Thread-0,5,main]
String name = t.getName();
System.out.println(name);*/

System.out.println(Thread.currentThread().getName()); // 链式编程

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package cn.perdant.day15.demo03;
/*
* 线程的名称:
* 主线程:main
* 新线程:Thread-0,Thread-1,Thread-2 ...*/
public class Demo02Thread {
public static void main(String[] args) {
MyThread mt = new MyThread();
// mt.start(); // Thread-0
new MyThread().start(); // Thread-1
new MyThread().start(); // Thread-2
System.out.println(Thread.currentThread().getName()); // main
mt.setName("伍陆柒");
mt.start(); // 伍陆柒
new MyThread("梅十三").start(); //梅十三

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package cn.perdant.day15.demo03;
/*
* public static void sleep(long millis): 使当前正在执行的线程以指定的毫秒数暂时停止执行
* 毫秒数结束之后,线程继续执行
*/
public class Demo01Sleep {
public static void main(String[] args) {
for (int i = 1; i <= 60 ; i++) {
System.out.println(i);

//使用Thread类的sleep方法,让程序睡眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

方式二:实现Runnable接口

1
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 cn.perdant.day15.demo03;
/*
* 创建多线程程序的第二种方式:实现Runnable接口
* 实现步骤:
* 1. 创建一个Runnable接口的实现类
* 2. 在实现类中重写Runnable接口的run方法,设置线程任务
* 3. 创建一个Runnable接口的实现类对象
* 4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
* 5. 调用Thread类中的start方法,开启新的线程执行run方法
* 好处:
* 1. 避免了单继承的局限性
* 一个类只能继承一个类,类继承了Thread类就不能继承其他的类了
* 实现了Runnable接口,还可以继承其他的类,实现其他的接口
* 2. 增强了程序的扩展性,降低了程序的耦合性(解耦)
* 实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)
* 实现类中,重写了run方法,用来设置线程任务
* 创建Thread类对象,调用start方法,用来开启新线程
*/
public class Demo01Runnable {
public static void main(String[] args) {
// 3. 创建一个Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);
// 5. 调用Thread类中的start方法,开启新的线程执行run方法
t.start();
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package cn.perdant.day15.demo03;

// 1. 创建一个Runnable接口的实现类
public class RunnableImpl implements Runnable {
// 2. 在实现类中重写Runnable接口的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}

以后多线程,尽量使用实现Runnable接口的方式

匿名内部类方式实现线程创建

1
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 cn.perdant.day15.demo03;
/*
* 匿名内部类作用:简化代码
* 把子类继承父类,重写父类的方法,创建子类对象合一步完成
* 把实现类实现接口,重写接口中的方法,创建实现类对象合一步完成
* 匿名内部类最终产物:子类/实现类对象,而这个类没有名字
*
* 格式:
* new 父类/接口(){
* 重写父类/接口中的方法
* }
*/
public class Demo01InnerClassThread {
public static void main(String[] args) {
// 继承Thread类的方式
new Thread() {
@Override
public void run() {
for (int i = 0; i <20 ; i++) {
System.out.println(Thread.currentThread().getName()+ i);
}
}
}.start();

// 实现Runnable接口的方式
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
};

new Thread(r).start();

// 更加简化接口的方式
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}).start();

}
}

线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

线程安全问题概述

线程安全问题的概述

线程安全问题示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package cn.perdant.day15.demo04;
/*
* 模拟卖票案例
* 创建三个线程,同时开启*/
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}
1
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 cn.perdant.day15.demo04;

/*实现卖票案例*/
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
// 设置线程任务 买票
@Override
public void run() {
// 使用死循环,卖票操作重复执行
while (true) {
if (ticket>0){
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票
System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticket+"张票");
ticket--;
}
}
}
}

运行结果: 运行结果

线程安全问题产生原理分析: 线程安全问题产生的原理

为了解决解决线程安全问题采用线程同步技术

同步技术的原理: 同步的原理 这张图要结合下面的几种技术的代码例子来理解

技术一:同步代码块

1
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 cn.perdant.day15.demo05;

/*
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票

解决线程安全问题的一种方案:使用同步代码块
格式:
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意:
1. 通过代码块种的锁对象,可以使用任意的对象
2. 但是必须保证多个线程使用的锁对象是同一个
3. 锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块种执行

*/
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
//创建一个锁对象
Object obj = new Object();
// 设置线程任务 买票
@Override
public void run() {
// 使用死循环,卖票操作重复执行
while (true) {
// 创建一个同步代码块
synchronized (obj){
if (ticket>0){
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票
System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticket+"张票");
ticket--;
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.perdant.day15.demo05;

/*
* 模拟卖票案例
* 创建三个线程,同时开启*/
public class Demo01Ticket {
public static void main(String[] args) {
RunnableImpl run = new RunnableImpl();
Thread t0 = new Thread(run);
Thread t1 = new Thread(run);
Thread t2 = new Thread(run);
t0.start();
t1.start();
t2.start();
}
}

38.3 技术二:同步方法

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

/*
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票
解决线程安全问题的一种方案:使用同步方法
使用步骤:
1. 把访问了共享数据的代码抽取出来,放到一个方法中
2. 在方法上添加synchronized修饰符
格式:
修饰符 synchronized 返回值类型 方法名(参数列表){
访问了共享数据的代码
}
*/
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;

//创建一个锁对象
Object obj = new Object();

// 设置线程任务 买票
@Override
public void run() {
// 使用死循环,卖票操作重复执行
while (true) {
// 调用同步方法
payTicket();
}
}

/*
定义一个同步方法
同步方法的原理本质和同步代码块一样
同步方法也会把方法内部的代码锁住只让一个线程执行
同步方法的锁对象就是this 也就是这里定义的类 RunnableImpl new的实例对象!
相比同步代码块,同步方法不需要在RunnableImpl类中单独new一个锁对象。
*/
public synchronized void payTicket() {
if (ticket > 0) {
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 票存在,卖票
System.out.println(Thread.currentThread().getName() + "正在卖第:" + ticket + "张票");
ticket--;
}
}
}

静态同步方法: 静态同步方法的锁对象不是this,因为this是创建对象之后产生的,静态方法优先于对象 静态方法的锁对象是本类的class属性 --> class文件对象(反射,后面会讲)

技术三:Lock锁技术

JDK1.5后的新接口java.util.concurrent.locks

1
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 cn.perdant.day15.demo07;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/*
卖票案例出现了线程安全问题
卖出了不存在的票和重复的票

解决线程安全问题的一种方案:使用Lock锁
比synchronized更加先进好用
两个方法lock() unlock()
实现类ReentrantLock
使用步骤:
1. 在成员位置创建一个ReentrantLock对象
2. 可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
3. 可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
*/
public class RunnableImpl implements Runnable {
// 定义一个多个线程共享的票源
private int ticket = 100;
//1. 在成员位置创建一个ReentrantLock对象
Lock l = new ReentrantLock();
// 设置线程任务 买票
@Override
public void run() {
// 使用死循环,卖票操作重复执行
while (true) {
// 2. 可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
l.lock();
if (ticket>0){
// 提高安全问题出现的概率,让程序睡眠
try {
Thread.sleep(100);
// 票存在,卖票
System.out.println(Thread.currentThread().getName()+"正在卖第:"+ticket+"张票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 3. 可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
l.unlock(); // 无论程序是否异常,都会把锁释放
}
}

}
}
}

线程状态

线程的状态图

计时等待(Timed Waiting) 计时等待

锁阻塞(Blocked) 锁阻塞

无限等待(Waiting) 无限等待

等待唤醒机制

等待唤醒案例分析
1
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
package cn.perdant.day15.demo08;


import java.util.concurrent.ThreadLocalRandom;

/*
等待唤醒案例:线程之间的通信
创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
注意:
顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个执行
同步使用的锁对象必须保证唯一
只有锁对象才能调用wait和notify方法
这两个方法都来自Object类
*/
public class Demo01WaitAndNotify {
public static void main(String[] args) {
// 创建锁对象
Object obj = new Object();
// 创建顾客线程
new Thread(){ // 匿名内部类
@Override
public void run() {
while (true) {
synchronized (obj) {
System.out.println("告知老板包子的种类和数量");
try {
//调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
obj.wait(); // 这里只能try...catch不能throws 因为其父类Thread没有throws!
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒之后执行的代码
System.out.println("包子好了 开吃!");
System.out.println("=================");
}
}
}
}.start();

// 创建老板线程
new Thread(){
@Override
public void run() {
while (true) {
// 5秒钟做包子
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("老板做好包子,告知顾客");
obj.notify();
}
}
}
}.start();
}
}

上面的代码示例就是先熟悉一下锁对象调用的两个方法:waitnotify 记住!这俩是锁对象调用的! 线程类调用的是start sleep currentThread

各人见解: waitnotify 进入的这种无线等待模式的好处是什么呢? 之前卖票的例子里我们可以看到,虽然加入了同步技术解决了线程安全问题,但是,线程的运行顺序是没有规则的,谁抢到cpu谁就运行,但是如果我规定,奇数票必须第一个线程买,偶数票第二个线程卖呢?如果使用waitnotify 就可以让线程一在票数为奇数的时候notify 不是奇数的时候 wait 线程二同理。这样就可以让线程有一个规则顺序执行。(你们可以自己试一下卖票这个题,我自己写了一个,参见39.3 练习

这也就是下面要讲的:线程间的通信。

线程间通信

概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。 例如前面的包子案例,顾客线程吃包子,老板线程做包子

为什么要处理线程间通信: 多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源: 多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即—— 等待唤醒机制。

代码案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cn.perdant.day15.demo09;
/*
资源类:包子类
设置包子的属性


包子的状态:有 true 没有 false
*/
public class BaoZi {
// 皮
String pi;
// 陷
String xian;
// 包子的状态:有 true 没有 false
boolean flag = false;
}
1
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
package cn.perdant.day15.demo09;

/*
生产者 包子铺 类: 是一个线程类
run:
有包子 wait
没有包子 生产包子 改变包子状态 唤醒吃货线程
注意:
包子铺线程和吃货线程是通信(互斥)关系
必须同时同步技术保证两个线程只能有一个在执行
锁对象必须保证唯一,可以使用包子对象作为锁对象
包子铺类和吃货类需要把包子对象作为参数传递进来
1. 需要在成员位置创建一个包子变量
2. 使用带参数的构造方法,为这个包子变量赋值
*/
public class BaoZiPu extends Thread {
// 1. 需要在成员位置创建一个包子变量
private BaoZi bz;

public BaoZiPu() {
}

// 2. 使用带参数的构造方法,为这个包子变量赋值
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}

// 设置线程任务
@Override
public void run() {
int count = 0;
// 一直生产包子
while (true) {
synchronized (bz) {
if (bz.flag == true) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒之后执行 包子铺生产包子
// 增加趣味性,交替生产两种包子
if (count % 2 == 0) {
// 生产薄皮包子
bz.pi = "薄皮";
bz.xian = "三鲜馅";
} else {
bz.pi = "冰皮";
bz.xian = "牛肉大葱馅";
}
count++;
System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "包子");
// 生产包子需要3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 生产好后 修改包子的状态
bz.flag = true;
// 唤醒吃货线程
bz.notify();
System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "包子,吃货可以开始吃了!");
}
}
}
}
1
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 cn.perdant.day15.demo09;
/*
类似包子铺类,定义一个吃货类

*/
public class ChiHuo extends Thread {
// 1. 需要在成员位置创建一个包子变量
private BaoZi bz;

public ChiHuo() {
}

// 2. 使用带参数的构造方法,为这个包子变量赋值
public ChiHuo(BaoZi bz) {
this.bz = bz;
}
// 设置线程任务 吃包子
@Override
public void run() {
while(true){
synchronized (bz){
if (bz.flag == false){
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒之后 执行的代码
System.out.println("吃货正在吃:"+ bz.pi + bz.xian + "包子");
// 吃完修改包子状态
bz.flag = false;
// 唤醒包子铺线程
bz.notify();
System.out.println("吃货已经把"+ bz.pi + bz.xian + "包子吃完了,包子铺开始生产");
System.out.println("=============================================================");
}
}

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.perdant.day15.demo09;

public class Demo {
public static void main(String[] args) {
// 包子对象
BaoZi bz = new BaoZi();
// 包子铺线程
new BaoZiPu(bz).start();
// 吃货线程
new ChiHuo(bz).start();

}
}

练习

1
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
// 票
public class Ticket {
public int tickets = 100;
boolean flag = true;
}

//售票窗口1: 只卖偶数票
public class SellTicket1 implements Runnable {
private Ticket ticket;

public SellTicket1() {
}

public SellTicket1(Ticket ticket) {
this.ticket = ticket;
}

@Override
public void run() {
while (ticket.flag) {
synchronized (ticket) {
if (ticket.tickets%2 == 1 && ticket.tickets > 0){
try {
ticket.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (ticket.tickets > 0){
System.out.println("窗口1正在卖第:" + ticket.tickets + "张票");
ticket.tickets--;
ticket.notify();
}else {
System.out.println("窗口1票卖完了");
ticket.flag = false;
}
}
}
}
}

// 售票窗口2:只卖奇数票
public class SellTicket2 implements Runnable {
private Ticket ticket;

public SellTicket2() {
}

public SellTicket2(Ticket ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (ticket.flag) {
synchronized (ticket) {
if (ticket.tickets % 2 == 0 && ticket.tickets > 0){
try {
ticket.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (ticket.tickets > 0){
System.out.println("窗口2正在卖第:" + ticket.tickets + "张票");
ticket.tickets--;
ticket.notify();
}else {
System.out.println("窗口2票卖完了");
ticket.flag = false;
}
}
}
}
}

// 主程序卖票
public class Demo {
public static void main(String[] args) {
Ticket tc = new Ticket();
SellTicket1 s1 = new SellTicket1(tc);
SellTicket2 s2 = new SellTicket2(tc);
Thread t0 = new Thread(s1);
Thread t1 = new Thread(s2);
t0.start();
t1.start();
}
}

上述记录于1029.11.14

线程池

线程池思想概述

我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。 那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务? 在Java中可以通过线程池来达到这样的效果。

线程池概念

线程池:其实就是一个容纳多个线程的容器(还记得前面学的容器吗? Collection Map ),其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

线程池的工作原理: 线程池原理

合理利用线程池能够带来三个好处: 1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。 2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量) 获取到了一个线程池ExecutorService 对象,那么怎么使用呢? 方法如下:
  • public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
  • void shutdown() 关闭/销毁线程池的方法 > Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

使用线程池中线程对象的步骤: 1. 创建线程池对象。 2. 创建Runnable接口子类对象。(task) 3. 提交Runnable接口子类对象。(take task) 4. 关闭线程池(一般不做)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo01ThreadPool {
public static void main(String[] args) {
// 1. 创建线程池对象
ExecutorService es = Executors.newFixedThreadPool(2);
// 3. 提交Runnable接口子类对象
es.submit(new RunnableImpl()); // pool-1-thread-2创建了一个新的线程执行
es.submit(new RunnableImpl()); // pool-1-thread-1创建了一个新的线程执行
// 线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程池可以继续使用
es.submit(new RunnableImpl()); // pool-1-thread-1创建了一个新的线程执行
// 4. 关闭线程池(一般不做)。
es.shutdown();
// es.submit(new RunnableImpl()); // 抛出异常,线程池没有了,不能获取线程了
}
}
1
2
3
4
5
6
7
// 2. 创建Runnable接口子类对象
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"创建了一个新的线程执行");
}
}

线程池底层原理: 线程池底层原理

Lambda 表达式

函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

面向对象的思想:做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情 函数式编程思想:只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

传统写法

1
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 cn.perdant.day15.demo11;

import java.util.concurrent.ThreadLocalRandom;

/*
使用实现Runnable接口的方式实现多线程程序
*/
public class Demo01Runnable {
public static void main(String[] args) {
// 创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
// 创建Thread类对象,构造方法中传递Runnable接口的实现类
Thread t= new Thread(run);
// 调用start方法开启新线程,执行run方法
t.start();

// 简化代码,使用匿名内部类实现多线程程序
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
};

new Thread(r).start();

// 继续简化上面的代码
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
}).start();

}


}
1
2
3
4
5
6
7
8
9
10
11
package cn.perdant.day15.demo11;
/*
创建Runnable接口的实现类,重写run方法,设置线程任务
*/
public class RunnableImpl implements Runnable{

@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"新线程创建了");
}
}

对于Runnable的匿名内部类用法(详情见 37.7)可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

编程思想转换

做什么,而不是怎么做

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。 2014年3月Oracle所发布的Java 8(JDK 1.8)中,加入了Lambda表达式的重量级新特性,为我们打开了新世界的大门。

Lamda写法

1
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 cn.perdant.day15.demo11;
/*
Lambda省去面向对象的条条框框,格式由**3个部分**组成:
一些参数
一个箭头
一段代码
格式:
(参数类型 参数名称) -> { 代码语句 }
格式说明:
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
`->`是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。
*/
public class Demo02Lamda {
public static void main(String[] args) {
// 使用匿名内部类的方式,实现多线程
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
}).start();

// 使用lamda表达式,实现多线程
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "新线程创建了");
}
).start();
}
}
1
2
3
4
5
package demo12;

public interface Calculator {
public abstract int calc(int a, int b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package demo12;

public class Demo01Calculator {
public static void main(String[] args) {
invokeCalc(10, 20, new Calculator() { // 匿名内部类,实现Calculator接口
@Override
public int calc(int a, int b) {
return a+b;
}
});

// 使用Lamda表达式
invokeCalc(120, 130, (int a,int b)->{ // 接口名,方法名 都省略了! 比匿名内部类还简洁。
return a+b;
});
}

public static void invokeCalc(int a, int b, Calculator c){
int sum = c.calc(a,b);
System.out.println(sum);
}
}

Lambda省略格式

可推导即可省略:

可推导可省略类似之前讲到的泛型,泛型在赋值号右边new出来的泛型可以不用写,因为赋值号左边写了,可以推导出右边的泛型。 Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

1
2
3
public static void main(String[] args) {
invokeCalc(120, 130, (a, b) -> a + b); // 参数类型,return 大括号和分号全都省略了!
}

Lambda标准格式的省略规则:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。必须一起省略!

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。

但是使用时有几个问题需要特别注意: 1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。 无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。 2. 使用Lambda必须具有上下文推断 也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例 > 备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

上述记录于2019.11.15

File类

概述

java.io.File 类:Java把电脑中的文件和文件夹(目录)封装为一个File类,与操作系统无关,我们可以使用File类对文件和文件夹进行操作:

  • 创建一个文件/文件夹
  • 删除一个文件/文件夹
  • 获取一个文件/文件夹
  • 判断文件/文件夹是否存在
  • 对文件夹进行遍历
  • 获取文件的大小

上述记录于2019.11.19

静态成员变量

File.pathSeparator : 路径分隔符 在配置环境变量里提到过path里面就用到了这种分隔符 File.separator:名称分隔符

代码示例:

1
2
3
4
5
6
7
8
9
10
import java.io.File;

public class Demo01File {
public static void main(String[] args) {
String pathSeparator = File.pathSeparator; // 与系统有关的路径分隔符
System.out.println(pathSeparator); // ; 路径分隔符 Windows:分号,Linux:冒号
String separator = File.separator; // 与系统有关的默认名称分隔符
System.out.println(separator); // \ 文件名称分隔符 Windows:反斜杠\,Linux:正斜杠/
}
}

路径

绝对路径:是一个完整的路径,以盘符(C:,D:)开始的 相对路径:是一个简化的路径,如果文件存储在与当前项目同一个的根目录下的话,可以不写根目录之前的路径,只写根目录之后的路径。

什么是根目录,比如我后面反复用到的路径"D:\\IdeaProjects\\basic-code\\..." 这里我写的所有代码都是basic-code目录下建立的各种module中,所以这里的根目录是basic-code,只要是在basic-code目录下的文件/文件夹,basic-code之前的路径,包括basic-code都可以省略。

注意:

  1. 路径不区分大小写
  2. 路径中的文件名称分隔符Windows使用反斜杠\\,单个反斜杠是转义字符,所以要用两个反斜杠代表一个普通的反斜杠

构造方法

  1. File(String pathname) 参数:字符串的路径名称
  • 可以以文件结尾,也可以是文件夹结尾
  • 可以是绝对路径,也可以是相对路径
  • 可以是存在的,也可以是不存在
  1. 创建File对象,只是把字符串路径封装为File对象,不考虑路径的真假情况说白了,你路径可以随便写,只要符合路径定义的格式就可以了,这里编译器是不会去处理判断你的文件是否存在,你写的路径是否正确等等。因为你还没用这个file类实例做任何操作。

  2. File(String parent, String child) 参数:父路径和子路径 好处:可以单独书写,使用起来非常灵活,父子路径都可以变化

  3. File(File parent, String child) 参数:父路径是File类型,可以使用File类的方法,对路径进行一些操作,再使用路径创建对象

获取的方法

  1. public String getAbsolutePath() :返回此File的绝对路径名字符串

  2. public String getPath():获取构造方法中传递的路径。 构造方法中用的是绝对路径,就返回绝对路径,用的是相对路径,就返回相对路径。toString 的源码调用的就是 getPath方法

  3. public String getName() :获取构造方法传递的路径的结尾部分,就是只返回路径的最后一层的文件/文件夹

  4. public long length() :获取的是构造方法指定的文件的大小,以字节为单位。注意:文件夹是没有大小概念的,不能获取文件夹的大小,方法返回0;如果构造方法中给出的路径不存在,也返回0

判断的方法

  1. public boolean exists() :用于判断构造方法中的路径是否存在

  2. public boolean isDirectory() :用于判断构造方法中的路径是否以文件夹结尾

  3. public boolean isFile() :与上一方法相反,用于判断构造方法中的路径是否以文件结尾

  4. 2、3 方法是互斥的,一个返回true另一个就返回false。但是如果使用这两个方法时file构造时传递的路径是不存在的,都返回false

创建/删除的方法

  1. public boolean createNewFile()
  • 当且仅当具有该名称的文件尚不存在时,创建一个新的空文件,返回true
  • 如果文件存在或者有同名的文件夹存在,返回false
  • 创建文件的路径和名称,在构造方法中给出(构造方法的参数)
  • 如果路径不存在,抛出IOException,因此调用时必须处理异常。try...catch or throws
  • 此方法只能创建文件,不能创建文件夹。
  1. public boolean delete()
  • 删除构造方法路径给出的文件/文件夹
  • 文件/文件夹 删除成功 返回true
  • 文件夹中有内容,不会删除,返回false
  • 构造方法中的路径不存在,返回false
  • 此方法直接在硬盘删除文件/文件夹,不走回收站
  1. public boolean mkdir() :创建单级空文件夹

  2. public boolean mkdirs() :既可以创建单级空文件夹,也可以创建多级空文件夹。

  3. 对于3、4两种方法:

  • 如果文件夹不存在,创建并返回true
  • 如果文件夹存在,返回false;如果路径不存在,也返回false
  • 只能创建文件夹,不能创建文件。就算你路径里写的xxx.txt,创建的照样是一个名字为 xxx.txt的文件夹,这里的.txt并不是文件后缀,只是当作文件夹名字来处理了

目录的遍历

  1. public String[] list() :遍历构造方法中给出的目录,会获取目录下所有文件/文件夹的名称,把获取到的文件/文件夹的名称存储到一个 String[] 数组里。

  2. public File[] listFiles() :遍历构造方法中给出的目录,会获取目录下所有文件/文件夹的名称,把获取到的文件/文件夹封装为File对象,存储到一个 File[] 数组里。

  3. 对于1、2两种方法:

  • 如果路径不存在或者不是一个合法路径,抛出空指针异常NullPointerException
  • 可以遍历到隐藏的文件/文件夹

递归

概述

递归:指在当前方法内调用自己的这种现象。

递归的分类

递归分为两种:

直接递归:方法自身调用自己。 间接递归:A方法调用B方法,B方法调用C方法,C方法调用A方法。

注意事项

  • 递归一定要有条件限定,保证递归能够停止下来,否则会发生栈内存溢出StackOverflowError(方法进栈,然后再次调用方法自身,只有进栈,没有出栈。就会导致栈内存中有无数个方法。从而超出栈内存的大小。)

  • 在递归中虽然有限定条件,但是递归次数不能太多。否则也会发生栈内存溢出。

  • 构造方法,禁止递归,否则编译报错。(构造方法是创建对象使用的,递归会导致内存中有无数多个对象!)

递归的使用前提:当调用方法的时候,方法的主体不变,每次调用方法的参数不同

使用递归必须明确:

  1. 递归的结束条件
  2. 递归的目的

练习

计算1 ~ n的和

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class Demo02Recurison {
public static void main(String[] args) {
int s = sum(3);
System.out.println(s);
}
public static int sum(int n) {
if (n == 1){ // 1. 递归的结束条件
return 1;
}
return n + sum(n-1); // 2. 递归的目的
}
}

n的阶乘

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Demo03Recurison {
public static void main(String[] args) {
int a = jc(5);
System.out.println(a);
}

private static int jc(int n) {
if (n == 1){ // 1. 递归的结束条件
return 1;
}
return n*jc(n-1); // 2. 递归的目的
}
}

递归打印多级目录

如何打印一个目录下的所有文件和文件夹,并且继续打印文件夹里面的文件和文件夹。直到打印到没有文件夹只有文件或空文件夹的底层。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Demo04Recurison {
public static void main(String[] args) {
File file = new File("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17"); // 电脑里的任意一个想遍历的目录路径
getAllFile(file);
}

private static void getAllFile(File file) {
System.out.println(file); // 先打印当前的目录
File[] files = file.listFiles(); // 将当前目录下的文件/文件夹存入数组
for (File f : files) { // 遍历数组
if (f.isDirectory()) {
getAllFile(f); // 如果遍历的是文件夹,迭代
} else {
System.out.println(f); // 反之,是文件,直接打印
}
}
}
}

我们再进一步思考:如果我并不是想仅仅遍历多级目录,而是想在遍历完这些目录之后,只打印.java结尾的文件呢?其实也简单,去掉打印当前目录的那一步,然后在判断是文件后再加一步检验文件名是否以.java结尾即可。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.File;

public class Demo05Recurison {
public static void main(String[] args) {
File file = new File("day17-code\\src\\cn\\perdant\\day17"); // 电脑里的任意一个想遍历的目录路径
getAllFile(file);
}

private static void getAllFile(File file) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isDirectory()) {
getAllFile(f);
} else {
if (f.toString().toLowerCase().endsWith(".java")) { // 这里多添加一步判断,如果以.java结尾的话(这里加了一步可以忽略大小写),打印!
System.out.println(f);
}
}
}
}
}

文件过滤器

上面这种选择性打印是比较简单的情况,如果遇到复杂的情况呢?如果我们想让这种规则适用于其他的已经写好的遍历文件的代码里呢?这样一次次的修改代码是不是太麻烦了? 这里,我们引入了一种新的接口:文件过滤器FileFilterFilenameFilterFile类中有两个ListFiles的重载方法,方法的参数传递的就是过滤器

  1. File[] listFiles(FileFileter filter)
    • java.io.FileFilter 接口:用于过滤File对象
      • 其中的抽象方法: boolean accept(File pathname) 用来过滤文件的方法
      • 参数:pathname 使用listFiles遍历目录得到的每一个File对象
  2. File[] listFiles(FilenameFileter filter)
    • java.io.FilenameFileter 接口:用于过滤文件名称
      • 其中的抽象方法:boolean accept(File dir, String name) 用来过滤文件的方法
      • 参数:
        • dir:调用listFilesFile对象(这道题里就是我们一开始给的要遍历的目录封装成的File对象);
        • name:使用listFiles遍历目录得到的每一个文件/文件夹的名称。

注意:

两个过滤器接口都没有实现类,需要我们自己写实现类,重写过滤的方法accept,在方法中自己定义过滤的规则。

我们可以把上节的代码使用文件过滤器再一步优化:

代码优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.File;
import java.io.FileFilter;

/*
创建过滤器FileFilter的实现类
*/
public class FileFilterImpl implements FileFilter {
@Override
public boolean accept(File pathname) { // 重写accept方法,设置过滤规则
if (pathname.isDirectory()){
return true; // 如果是文件夹的话,返回true,可以把该File对象存入数组,方便后面迭代
} else {
// 如果文件名不是以.java结尾,返回false。不允许该File对象存入数组
return pathname.getName().toLowerCase().endsWith(".java");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.File;

public class Demo06Recurison {
public static void main(String[] args) {
File file = new File("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17"); // 电脑里的任意一个想遍历的目录路径
getAllFile(file);
}

private static void getAllFile(File file) {
File[] files = file.listFiles(new FileFilterImpl()); // 参数传递过滤器实现类对象
// 过滤器实现类accept方法在这里发力了!通过我们重写的规则来决定谁进了File[]数组 谁没进
for (File f : files) {
if (f.isDirectory()) {
getAllFile(f);
} else {
System.out.println(f);
}
}
}
}

文件过滤器原理

看不懂代码的话,可以先看看这张原理图,为方便理解,这里的accept规则是return true,就相当于没有过滤,和没加过滤器的效果一样。

FileFilter过滤器的原理

终极优化版

结合之前学到的匿名内部类Lamda表达式 我们可以把代码再一次化简,这里我们加上listFiles的另一种重载方法的用法。

代码优化:

1
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 Demo07Recurison {
public static void main(String[] args) {
File file = new File( "D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17");
getAllFile(file);
}
// 传递FilenameFileter的listFiles方法
private static void getAllFile(File file) {
File[] files = file.listFiles(
(dir, name) -> new File(dir,name).isDirectory() || name.toLowerCase().endsWith(".java") // Lamda表达式省略版写法!注意:这里为了判断是否是文件夹,需要先把dir和name封装成一个File对象,这里用到了前面讲的一种构造方法,父路径File类型的那个。
);
// 下面迭代操作不变
for (File f : files) {
if (f.isDirectory()) {
getAllFile(f);
} else {
System.out.println(f);
}
}
}

/*
// 传递FileFileter的listFiles方法
private static void getAllFile(File file) {
File[] files = file.listFiles(pathname -> pathname.isDirectory() || pathname.getName().toLowerCase().endsWith(".java"));
for (File f : files) {
if (f.isDirectory()) {
getAllFile(f);
} else {
System.out.println(f);
}
}
}*/
}

上述记录于2019.11.20

IO概述

什么是IO

数据的传输,可以看做是一种数据的流动,按照流动的方向,以内存为基准,分为输入input输出output ,即流向内存是输入流,流出内存的输出流。

JavaI/O操作主要是指使用java.io包下的内容,进行输入、输出操作。输入也叫做读取数据,输出也叫做作写出数据。

IO的分类

根据数据的流向分为:输入流输出流

  • 输入流 :把数据从其他设备上读取到内存中的流。
  • 输出流 :把数据从内存 中写出到其他设备上的流。

格局数据的类型分为:字节流字符流

  • 字节流 :以字节为单位,读写数据的流。
  • 字符流 :以字符为单位,读写数据的流。

顶级父类

输入流 输出流
字节流 字节输入流
InputStream
字节输出流
OutputStream
字符流 字符输入流
Reader
字符输出流
Writer

字节流

一切皆为字节

一切文件数据(文本、图片、视频等)在存储时,都是以二进制数字的形式保存,8位二进制数就是一个字节,传输时一样如此。所以,字节流可以传输任意文件数据。在操作流的时候,要时刻明确,无论使用什么样的流对象,底层传输的始终为二进制数据。

字节输出流 OutputStream

java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的共性方法。

public void close() :关闭此输出流并释放与此流相关联的任何系统资源。 public void flush() :刷新此输出流并强制任何缓冲的输出字节被写出。 public void write(byte[] b):将 b.length字节从指定的字节数组写入此输出流。 public void write(byte[] b, int off, int len) :从指定的字节数组写入 len字节,从偏移量 off开始输出到此输出流。 public abstract void write(int b) :将指定的字节输出流。

文件字节输出流 FileOutputStream

OutputStream有很多子类,下面介绍其最简单的一个子类java.io.FileOutputStream文件输出流,用于将数据写出到文件。

输出数据的原理:

java程序 --> JVM(虚拟机)--> OS(操作系统)--> OS调用写数据的方法,把数据写入到文件中

JVM起到一个中介作用,因为不一样的操作系统,文件的某些特殊字符的使用方式是不同的,比如后面要讲的结束符,换行符等等,所以先要将数据交给JVMJVM将数据处理成OS可以接收的形式,然后OS再与文件做交互,将数据写入文件

构造方法

  • public FileOutputStream(File file)

  • public FileOutputStream(String name)

    • 参数: 写入数据的目的地
      • String name :目的地是一个文件的路径
      • File file:目的地是一个文件

构造方法的作用:

  1. 创建一个FileOutputStream对象
  2. 会根据构造方法中传递的文件/文件路径,创建一个空的文件
  3. 会把FileOutputStream对象指向创建好的文件

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有这个文件,会创建该文件。如果有这个文件,会用写入后的文件覆盖原来的文件。

写出字节数据

字节输出流的使用步骤:

  1. 创建一个FileOutputStream对象,构造方法中传入数据的目的地
  2. 调用FileOutputStream对象中的方法write,把数据写入到文件中
  3. 释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提高程序的效率)

写出字节方法:

  1. void write(int b) ,写出一个字节数据

  2. void write(byte[] b),写出数组中的数据

  • 也可以用 String 类的 getBytes() 方法,将String类型解码成字节,getBytes()默认使用UTF-8的编码表。那我们打开文件想看到我们写的内容,也是必须用UTF-8编码格式打开,不然会乱码。后面转换流章节会详细解释

其实打开文件文本编辑器会将字节转换成字符,这步一般的文本编辑器都会自动根据文件内的字节来找到对应的编码表??

  1. write(byte[] b, int off, int len) , 写出数组中的部分数据
    • 参数:
      • 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
28
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class Demo01OutputStream {
public static void main(String[] args) throws IOException {
// 1. 创建一个FileOutputStream对象,构造方法中传入数据的目的地
FileOutputStream fos = new FileOutputStream("day17-code\\src\\cn\\perdant\\day17\\demo03\\a.txt");

// 2. 调用FileOutputStream对象中的方法write,把数据写入到文件中
// fos.write(49); // 字节49对应的是字符1
// fos.write(48); // 0
// fos.write(48); // 2

// 一次写多个字节
// byte[] bytes = {65, 66, 67, 68, 69}; // ABCDE UTF-8 编码
// byte[] bytes = {-65, -66, -67, 68, 69}; // 烤紻E 不是乱码。GBK编码 第一个字节是负数,会和第二个字节组成一个中文显示。
// byte[] bytes = {65, 66, 67, 68, 69};
// fos.write(bytes,1,2); // BC 写了数组的一部分 从 索引1 开始 写2个字节

byte[] bytes = "你好".getBytes(); // 这里把String变成字节数组
System.out.println(Arrays.toString(bytes)); // [-28, -67, -96, -27, -91, -67]
fos.write(bytes); // 你好

// 3. 释放资源(流使用会占用一定的内存,使用完毕要把内存清空,提高程序的效率)
fos.close();
}
}

追加续写

经过以上的演示,每次程序运行,创建输出流对象,都会清空目标文件中的数据。如何保留目标文件中数据,还能继续添加新数据呢?使用两个参数的构造方法

  • public FileOutputStream(File file, boolean append)
  • public FileOutputStream(String name, boolean append)

参数: String name,File file:写入数据的目的地 boolean append:追加写开关

  • true:创建对象不会覆盖原文件,继续在文件的末尾追加写数据
  • false:创建一个新文件,覆盖原文件

写出换行

使用换行符号:回车符 \r 和换行符号 \n 回车符:回到一行开头(return) 换行符:下一行(newline) 不同操作系统使用的方法不同。 - Windows: \r\n - Linux: /n - Mac: /r 但是从 Mac OS X 后开始与Linux统一

结合续写,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo03OutputStream {
public static void main(String[] args) throws IOException {
FileOutputStream fos = new FileOutputStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo03\\b.txt",true); // 把追加写开关打开!
for (int i = 0; i < 10; i++) { // 反复写10次
fos.write("你好".getBytes());
fos.write("\r\n".getBytes()); // 每次写完加上换行符号
}
fos.close();
}
}

字节输入流 InputStream

java.io.InputStream抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的共性方法。

public void close() :关闭此输入流并释放与此流相关联的任何系统资源。 public abstract int read(): 从输入流读取数据的下一个字节。 public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字节数组 b中

文件字节输入流 FileInputStream

仍然是介绍一个子类:java.io.FileInputStream类是文件输入流,用于从文件中读取字节。

输入数据的原理:

Java程序 --> JVM(虚拟机)--> OS(操作系统)--> OS调用读数据的方法,把文件的数据读取到内存中

构造方法

  • FileInputStream(File file)
  • FileInputStream(String name)

参数:读取文件的数据源 String name:文件的路径 File file: 文件

构造方法的作用: 1. 会创建一个FileInputStream对象 2. 会把FileInputStream对象指向构造方法中传入的参数的文件

当你创建一个流对象时,必须传入一个文件路径。该路径下,如果没有该文件,会抛出FileNotFoundException

读取字节数据

字节输入流的使用步骤:

  1. 创建FileInputStream对象,构造方法中,绑定要读取的数据源
  2. 使用FileInputStream对象中的方法read,读取文件
  3. 释放资源

读取字节方法

  1. int read():一次读取一个字节,提升为int型, 读取到文件末尾,返回-1
  2. int read(byte[] b):一次读取多个字节, 返回读取的有效字节个数

代码示例:

1
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
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;

public class Demo01InputStream {
public static void main(String[] args) throws IOException {
// 1. 创建FileInputStream对象,构造方法中,绑定要读取的数据源
FileInputStream fis = new FileInputStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo03\\a.txt"); // 这个文件里存的 abc
// 2. 使用FileInputStream对象中的方法read,读取文件
/* int len = fis.read();
System.out.println(len); // 97 读取到a,转换成int

len = fis.read();
System.out.println(len); // 98 读取到b 同理

len = fis.read();
System.out.println(len); // 99 读取到c

len = fis.read();
System.out.println(len); // -1 读取到结束符,到文件末尾了!*/

// 使用循环优化读取多个字节
// int len = 0; // 记录读取到的字节
// while ((len = fis.read()) != -1){ // 如果这次读取到的不是结束符
// System.out.println((char)len); // 打印这次的读取的字节对应的字符
// }
/*
// 一次读取多个字节 效率高
byte[] bytes = new byte[2];
int len = fis.read(bytes);
System.out.println(len); // 2 读取的有效字节个数
System.out.println(Arrays.toString(bytes)); // [97, 98]
System.out.println(new String(bytes)); // ab

len = fis.read(bytes);
System.out.println(len); // 1
System.out.println(Arrays.toString(bytes)); // [99, 98]
System.out.println(new String(bytes)); // cb

len = fis.read(bytes);
System.out.println(len); // -1 读取到的有效字节个数为0,说明读取到文件的结束符了。
System.out.println(Arrays.toString(bytes)); // [99, 98]
System.out.println(new String(bytes)); // cb
*/

// while 循环优化
byte[] bytes = new byte[1024];
int len = 0;
while((len = fis.read(bytes))!= -1){
System.out.println(new String(bytes,0,len)); // 我只要读取到的有效字节;因为数组剩下位置存的什么并不是这次读取到的字节,所以不用管。
}

// 3. 释放资源
fis.close();
}
}

字节流练习:图片复制

接下来,来一个结合输入输出流的练习。做一个简单的图片赋值,所有的文件都是字节,图片也不例外,我给大家提供一张我老婆的照片,要求:把我老婆的照片从C盘复制到D盘。

我老婆

新建一个输入流,新建一个输出流。 做一个循环,把每次readwrite出去。就ok啦。

代码:

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.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo01CopyFile {
public static void main(String[] args) throws IOException {
System.out.println(System.currentTimeMillis()); // 记录一个开始时间
FileInputStream fis = new FileInputStream("C:\\我老婆.jpg"); // 文件输入流 用来读取
FileOutputStream fos = new FileOutputStream("D:\\我老婆.jpg"); // 文件输出流 用来写出
int len = 0;
/* while((len = fis.read())!= -1) {
fos.write(len);
}*/
// 优化加速版
byte[] bytes = new byte[1024]; // 一次读取多个字节,复制速度更快
while((len = fis.read(bytes)) != -1){
fos.write(bytes,0,len);
}
fos.close(); // 先释放输出流,再释放输入流
fis.close(); // 因为咱们是读取一下在写出一下,如果读取完毕了,写出不一定完毕。但如果输出流用完了,输入流一定用完了。
System.out.println(System.currentTimeMillis()); // 记录一个结束时间
}
}

字符流

当使用字节流读取文本文件时,可能会有一个小问题。就是遇到中文字符时,可能不会显示完整的字符,那是因为一个中文字符可能占用多个字节存储(Unicod三个字节、GBK两个字节)。所以Java提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。 里面用到的各种方法使用步骤和字节流相似。

字符输入流 Reader

java.io.Reader抽象类是表示用于字符输入流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的共性方法。

public void close() :关闭此流并释放与此流相关联的任何系统资源。 public int read(): 从输入流读取一个字符。 public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到字符数组 cbuf中 。

FileReader类

java.io.FileReader extends InputStreamReader extends Reader

FileReader 文件字符输入流:把硬盘文件中的数据以字符的方式读取到内存中

构造方法

  • FileReader(File file)
  • FileReader(String fileName)

参数:读取文件的数据源

  • String fileName :文件的路径
  • File file:一个文件

作用:

  1. 创建一个FileReader对象
  2. 会把FileReader对象指向要读取的文件

使用步骤:

  1. 创建FileReader对象,构造方法中绑定要读取的数据源
  2. 使用FileReader对象中的方法read读取文件
  3. 释放资源

类似于FileInputStream

读取字符数据

  1. int read()
  2. int read(char[] cbuf)

这俩方法我就不解释了,和字节输入流的read方法类似只是获取到的一个是字节,一个是字符。

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.FileReader;
import java.io.IOException;

public class Demo01Reader {
public static void main(String[] args) throws IOException {
// 1. 创建FileReader对象,构造方法中绑定要读取的数据源
// 这个文件得是UTF-8编码,read读取的字符编码方式默认是UTF-8,两种码对应起来,才能不乱码
FileReader fr = new FileReader("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo04\\a.txt");
// 2. 使用FileReader对象中的方法read读取文件
// int len = 0;
// while ((len = fr.read()) != -1){ // 读取的是字符,只不过是提升成int类型
// System.out.print((char) len);
// }
// 一次读取多个字符,用字符数组保存,提高效率
char[] chars = new char[1024];
int len = 0;
while ((len = fr.read(chars)) != -1){
System.out.print(new String(chars,0,len)); // String类的构造方法,把字符数组的一部分转换为字符串
}
// 3. 释放资源
fr.close();
}
}

字符输出流 Writer

java.io.Writer抽象类是表示用于字符输出流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的共性方法。

void write(int c) 写入单个字符。 void write(char[] cbuf)写入字符数组。 abstract void write(char[] cbuf, int off, int len)写入字符数组的某一部分,off数组的开始索引,len写的字符个数。 void write(String str)写入字符串。 void write(String str, int off, int len) 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。 void flush()刷新该流的缓冲。 void close() 关闭此流,但要先刷新它。

FileWriter类

java.io.FileWriter extends OutputStreamWriter extends Writer FileWriter:文件字符输出流,把内存中的字符数据写入到文件中

构造方法

FileWriter(File file) FileWriter(String fileName)

参数:写入数据的目的地

  • String fileName:文件的路径
  • File file:文件

作用:

  1. 创建一个 FileWriter 对象
  2. 根据构造方法中传递的文件/文件的路径,创建文件
  3. 会把FileWriter对象指向创建好的文件

字符输出流使用步骤:

  1. 创建一个FileWriter对象,构造方法中绑定要写入数据的目的地
  2. 使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
  3. 使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
  4. 释放资源(会先把内存缓冲区中的数据刷新到文件中,因此第 3 步有时可以省略)

类似于FileOutputStream

基本写出数据

void write(int b):写出单个字符 void write(char[] cbuf):写出字符数组 void write(char[] cbuf, int off, int len): 写出字符数组的一部分 void write(String str):写字符串 void write(String str, int off, int len):写字符串的一部分

关闭和刷新

因为内置缓冲区的原因,如果不关闭输出流,无法写出字符到文件中。但是close 方法关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又想继续使用流,就需要flush 方法了。

void flush() :刷新缓冲区,流对象可以继续使用 void close():先刷新缓冲区,然后通知系统释放资源,流对象不可以再被使用

续写和换行

续写:在构造函数中加参数boolean append FileWriter(File file, boolean append) FileWriter(String fileName, boolean append)

换行:使用换行符,详情见字节流

上面三节内容和字节流部分极为相似,所以示例代码我整合着写成一块。

代码示例:

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

public class Demo01Writer {
public static void main(String[] args) throws IOException {
// 1. 创建一个FileWriter对象,构造方法中绑定要写入数据的目的地
FileWriter fw = new FileWriter("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo04\\a.txt", true); // 续写开关打开,支持续写

// 2. 使用FileWriter中的方法write,把数据写入到内存缓冲区中(字符转换为字节的过程)
// 写单个字符
fw.write(97); // a 因为单位是字符,就要先把字符存到内存缓冲区,缓冲区将字符解码为字节,如果不调用flush,数据依然再缓冲区,不会写到目标文件中!

// 写字符数组
char[] cs = {'a','b','c','d','e'};
fw.write(cs); // abcde

// 写字符数组的一部分
fw.write(cs,1,3); // bcd

// 写字符串
fw.write("刺客67"); // 刺客67

// 写字符串的一部分
fw.write("JOJO的黄金精神",5,4); // 黄金精神

// 写换行
fw.write("\r\n");

// 3. 使用FileWriter中的方法flush,把内存缓冲区中的数据,刷新到文件中
fw.flush();

// 刷新之后,流可以继续使用
fw.write(98); // b

// 4. 释放资源(会先把内存缓冲区中的数据刷新到文件中,因此第 3 步有时可以省略)
fw.close();
// fw.write(99); 流已经关闭抛出IOException:Stream closed
}
}

关于 字符/字节 输入/输出流的思考:

文件字节输出流这个过程就是:内存输入字节 --> 字节写入文件。但是write传入的是int型的参数,忽略了高24位,直接写入低8位,为什么不直接传入byte型的参数呢?为什么这么设计???我搜了一下资料,还真在知乎上找到了相同的问题,(!(https://www.zhihu.com/question/39814712))摘两个有意思的回答:

这个问题要从JVM虚拟机与底层交互的方面去分析,所以我觉得这是Java语言和c语言类型定义导致的问题; 首先看Java中的函数定义:private native void write(int b, boolean append) throws IOException; 注意:是 native 方法 C语言中的定义:int fputc(int ch,FILE *fp) 我们看到两种语言中都是使用int类型,而不是其它,为什么呢? 1、Java中有 byte 类型,C语言中没有 2、为什么不用char?java中bytechar需要一个强制转换,而 int不需要; 3、为什么不用short? 这个是32位CPU 4字节对齐的问题吧, 4、C语言中整型量和字符量可以通用;所以Java和C语言文件读写的通信,最佳类型就是int

大致意思就是,C语言中没有byte类型,所以用的参数int,而Java为了兼容C语言,做出了妥协

因为read()方法会返回 0~255 以及 -1。所以write()最好是设计成与read()相匹配的模式。 我们还可以注意到read(byte[])方法也会返回一个int代表读取到的字节数,同样也可能返回-1代表已到达流末尾。所以这里不需要使用int[],而是可以使用byte[]。 我想如果当初设计read()方法时,不是采用返回-1来代表流末尾,而是抛出一个异常EOFException的话,用byte也是完全可以的。

我想了下,也是,我read()返回的不单是byte啊,如果读取到结束位置,read返回的会是-1,所以read返回的数据范围一定会比byte1,那么我就得把read返回值设置成范围更大的int

而且read(byte[])这种重载方法也很巧妙,正常来讲,如果我把读取到的字节都存入byte[]的话,又会出现上面说的,如果读取到结束符呢??那不又要用-1来代替?这又要超byte的范围了,但是read(byte[])返回值设计成读取到的有效的字节个数,这就很有意思了,如果读取到结束符,我不存byte[]数组,而是统计一下读取到结束符前有几个字节数,把它返回,如果一个字节都没有读取到,返回-1。这样我的传入参数不需要是int[] 而是byte[]即可!

既然讲到了文件字节输入流的read,我们就接着编码问题在捋一捋文件字节输入流。读取文件内字节 --> 字节传递给内存。但是,到这一步,你得到的就是一串字节,如果你想在控制台打印文件里到底是什么,就需要将字节再编码为字符。可以使用new String(byte[] bytes)将字节数组转换成字符串,这一步就要注意了,字符串默认用的UTF-8的编码表,而文件内的字节呢,是跟文件当初怎么写怎么存的有关。

举个例子,我用GBK编码格式保存了一个文件,里面存了"你好" 两个字,如果按照上面说的流程,那控制台得到的结果就乱码了。因为字符串构造方法使用的编码(UTF-8)和源文件的编码(GBK)不一样。当然也有解决办法,详情见转换流

同理,文件字节输出流的write,如果我们使用字符串的getBytes()将字符串转换为字节,默认使用的也是UTF-8编码。 写出的文件如果用其他编码打开,也会乱码。

IO异常处理

JDK1.7之前,我们可以使用try..catch finally 处理流中的异常

1
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.io.FileWriter;
import java.io.IOException;

public class Demo02Writer {
public static void main(String[] args) {
// 提高变量fw的作用域,如果放到try里面的话,finally里不能使用。
// 变量在定义时可以没有值,但是使用的时候必须有值
// 如果不赋值,并且fw在try块里赋值失败,那么fw就没有值,finally块里的代码就会报错
FileWriter fw = null;
try {
// 可能产生异常的代码
fw = new FileWriter("E:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo04\\b.txt");
for (int i = 0; i < 10; i++) {
fw.write("欧拉" + i + "\r\n");
}
} catch (IOException e) {
// 异常的处理逻辑
e.printStackTrace();
} finally {
// 无论程序是否出现异常,close必须执行
// 如果fw创建失败,默认值null,null不能调用close,抛出NullPointerException
// 所以在此添加一步判断来优化代码,如果是null,就不执行close
if (fw != null) {
try {
// close方法声明抛出了IOException,也需要用try...catch处理
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

由上面的代码可以看到,try...catch finally 格式处理输入输出流是多么的麻烦,如何优化? JDK7的新特性,在try后面增加一个(),在括号中定义流对象。那么这个流对象的作用域,就在try中有效,try中代码执行完毕,会自动把流对象释放,不用写finally

JDK9的新特性,在try前面可以定义流对象,在try后边()中可以引入流对象的名称,在try代码执行完毕之后,流对象也可以释放掉,不用写finally

拿之前图片复制的代码举例:

1
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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo01JDK7 {
public static void main(String[] args) throws FileNotFoundException {
/*
// JDK7 新特性 try后面加括号
try (FileInputStream fis = new FileInputStream("C:\\我老婆.jpg");
FileOutputStream fos = new FileOutputStream("D:\\我老婆.jpg")) {
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
System.out.println(e);
} // 不需要finally 不需要close try执行完自动释放资源
*/

// JDK9 新特性 在try外面定义对象 注意定义对象的时候需要抛出 FileNotFoundException
FileInputStream fis = new FileInputStream("C:\\我老婆.jpg");
FileOutputStream fos = new FileOutputStream("D:\\我老婆.jpg");
try (fis;fos) {
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
} catch (IOException e) {
System.out.println(e);
}
}
}

属性集

概述

java.util.Properties extends Hashtable<k,v> implements Map<k,v> Properties 类来表示一个持久的属性集。Properties可保存在流中或从流中加载。该类也被许多Java类使用,比如获取系统属性时,System.getProperties 方法就是返回一个Properties对象。

Properties 是一个唯一和IO流相结合的集合

  • 可以使用Properties集合中的方法store 把集合中临时数据,持久化写入到硬盘中存储。
  • 可以使用Properties集合中的方法load 把硬盘中保存的文件(键值对),读取到集合中使用。
  • Properties集合是一个双列集合,keyvalue默认都是字符串。

Properties类

构造方法

  • public Properties() :创建一个空的属性列表。

基本的存储方法

  • Object setProperty(String key, String value) : 相当于Map集合中的put
  • String getProperty(String key) :相当于Map集合中的get
  • Set<String> stringPropertyNames() :相当于Map集合中的keySet

与流相关的方法

  • void store(OutputStream out, String comments)
  • void store(Writer writer, String comments)

参数: - OutputStream out: 字节输出流,不能写中文 - Writer writer:字符输出流,可以写中文 - String comments:注释,用来解释说明保存的文件是做什么用的,不能使用中文,会产生乱码,默认是Unicode编码,一般使用""空字符串

使用步骤: 1. 创建Properties集合对象,添加数据 2. 创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地 3. 使用Properties集合中的方法store,把集合中的临时数据持久化写入硬盘中存储 4. 释放资源

  • public void load(InputStream inStream)
  • public void load(Reader reader) 参数:
    • InputStream inStream:字节输入流,不能读取含有中文的键值对
    • Reader reader:字符输入流,能读取含有中文的键值对

使用步骤: 1. 创建Properties集合对象 2. 使用Properties集合对象中的方法load读取保存键值对的文件 3. 遍历Properties集合

注意: 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
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
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;

public class Demo01Properties {
public static void main(String[] args) throws IOException {
//show01();
//show02();
show03();
}


/*
使用Properties集合特有的方法遍历取出集合中的数据
*/
private static void show01() {
// 创建Properties集合对象
Properties prop = new Properties();

// 使用setProperties在集合中添加数据
prop.setProperty("乔斯达", "188");
prop.setProperty("承太郎", "188");
prop.setProperty("乔巴那", "188");

// 使用stringPropertyNames()把Properties集合中的键取出,存储到一个Set集合中
// 遍历Set集合,取出Properties集合的每一个键
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
// 使用getProperty(String key)通过key来获取value
String value = prop.getProperty(key);
System.out.println(key + "->" + value);
}
}

/*
store方法
*/
private static void show02() throws IOException {
// 1. 创建Properties集合对象
Properties prop = new Properties();

// 使用setProperties在集合中添加数据
prop.setProperty("乔斯达", "188");
prop.setProperty("承太郎", "188");
prop.setProperty("乔巴那", "188");

// 2. 创建字节输出流/字符输出流对象,构造方法中绑定要输出的目的地
FileWriter fw = new FileWriter("day17-code\\src\\cn\\perdant\\day17\\demo05\\a.txt");

// 3. 使用Properties集合中的方法store,把集合中的临时数据持久化写入硬盘中存储
prop.store(fw, "save data");

// 4. 释放资源
fw.close();
}

/*
load方法
*/
private static void show03() throws IOException {
// 1. 创建Properties集合对象
Properties prop = new Properties();
// 2. 使用Properties集合对象中的方法load读取保存键值对的文件
prop.load(new FileReader("day17-code\\src\\cn\\perdant\\day17\\demo05\\a.txt"));
// 3. 遍历Properties集合
Set<String> set = prop.stringPropertyNames();
for (String key : set) {
String value = prop.getProperty(key);
System.out.println(key + "=" + value);
}
}
}

缓冲流

概述

缓冲流,也叫高效流,是对4个基本的FileXxx 流的增强,所以也是4个流,按照数据类型分类:

字节缓冲流BufferedInputStreamBufferedOutputStream 字符缓冲流BufferedReaderBufferedWriter

缓冲流的基本原理,是在创建流对象时,会创建一个内置的默认大小的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。

字节缓冲输出流

java.io.BufferedOutputStream extends OutputStream BufferedOutputStream 字节缓冲输出流 继承了父类所有的共性方法

构造方法

public BufferedOutputStream(OutputStream out) public BufferedOutputStream(OutputStream out, int size)

参数: - OutputStream in:字节输出流,我们可以传递FileOutputStream,缓冲流会给FileOutputStream增加一个缓冲区,提高FileOutputStream的写出效率。 - int size:指定内部缓冲区的大小,不指定就是默认大小。

写出数据

步骤: 1. 创建一个FileOutputStream对象,构造方法中绑定要输出的目的地 2. 创建一个BufferedOutputStream对象,构造方法中传递FileOutputStream对象 3. 使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中 4. 使用BufferedOutputStream对象中的方法flush,把内部缓冲区中的数据,刷新到文件中 5. 释放资源(会先调用flush方法刷新,第 4 步可以省略)

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class Demo01BufferedOutputStream {
public static void main(String[] args) throws IOException {
// 1. 创建一个FileOutputStream对象,构造方法中绑定要输出的目的地
FileOutputStream fos = new FileOutputStream("day17-code\\src\\cn\\perdant\\day17\\demo06\\a.txt");
// 2. 创建一个BufferedOutputStream对象,构造方法中传递FileOutputStream对象
BufferedOutputStream bos = new BufferedOutputStream(fos);
// 3. 使用BufferedOutputStream对象中的方法write,把数据写入到内部缓冲区中
bos.write("我把数据写入到内部缓冲区中".getBytes());
// 4. 使用BufferedOutputStream对象中的方法flush,把内部缓冲区中的数据,刷新到文件中
bos.flush();
// 5. 释放资源(会先调用flush方法刷新,第 4 步可以省略)
bos.close(); // 不需要close掉fos,只要bos关闭,fos也就自动关闭了
}
}

字节缓冲输入流

java.io.BufferedInputStream extends InputStream BufferedInputStream 字节缓冲输入流

构造方法

public BufferedInputStream(InputStream in) public BufferedInputStream(InputStream in, int size)

参数: - InputStream in:字节输入流,我们可以传递FileInputStream,缓冲流会给FileInputStream增加一个缓冲区,提高FileInputStream的写出效率。 - int size:指定内部缓冲区的大小,不指定就是默认大小。

读取数据

步骤: 1. 创建FileInputStream对象 2. 创建BufferedInputStream对象,构造方法中传递FileInputStream对象 3. 使用BufferedInputStream对象的方法read 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
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;

public class Demo02BufferedInputStream {
public static void main(String[] args) throws IOException {
// 1. 创建FileInputStream对象
FileInputStream fis = new FileInputStream("day17-code\\src\\cn\\perdant\\day17\\demo06\\a.txt");
// 2. 创建BufferedInputStream对象,构造方法中传递FileInputStream对象
BufferedInputStream bis = new BufferedInputStream(fis);
// 3. 使用BufferedInputStream对象的方法read
// 一次读一个字节
// int len = 0;
// while ((len = bis.read()) != -1) {
// System.out.println(len);
// }

// 一次读多个字节
int len = 0;
byte[] bytes = new byte[1024];
while((len = bis.read(bytes)) != -1){
System.out.println(new String(bytes,0,len));
}
// 4. 释放资源
bis.close();
}
}

字符缓冲输出流

java.io.BufferedWriter extends Writer BufferedWriter 字节缓冲输出流

构造方法

public BufferedWriter(Writer out) public BufferedWriter(Writer out, int size)

特有成员方法

public void newLine(): 写一个行分隔符,会根据不同的操作系统,获取不同的行分隔符。 相比之前要根据不同操作系统写不一样的换行符,这个显然简单得多。 println方法调用的换行就是用的newLine

写出数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class Demo03BufferedWriter {
public static void main(String[] args) throws IOException {
BufferedWriter bw = new BufferedWriter(new FileWriter("day17-code\\src\\cn\\perdant\\day17\\demo06\\a.txt"));
for (int i = 0; i < 10; i++) {
bw.write("欧拉");
bw.newLine();
}
bw.flush();
bw.close();
}
}

字符缓冲输入流

java.io.BufferedReader extends Reader BufferedReader 字节缓冲输入流

构造方法

public BufferedReader(Reader in) public BufferedReader(Reader in, int size)

特有成员方法

public String readLine(): 读取一行文本行。读取一行数据。通过下列字符之一即可认为某行已终止:换行('\n')、回车('\r')或回车后直接跟着换行("\r\n"

返回值:包含该行内容的字符串但不包含任何行终止符;如果已达到流末尾,则返回null

读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class Demo04BufferedReader {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new FileReader("day17-code\\src\\cn\\perdant\\day17\\demo06\\a.txt"));
String s;
while((s = br.readLine()) != null){
System.out.println(s);
}
br.close();
}
}

练习:文本排序

请将文本信息恢复顺序。

1
2
3
4
5
6
7
8
9
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必得裨补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
4.将军向宠,性行淑均,晓畅军事,试用之于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
9.今当远离,临表涕零,不知所言。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐付托不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。

这里要用到正则表达式和Stringsplit 方法,现在不会写也没关系。看看步骤思想就好。

代码示例:

1
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.io.*;
import java.util.HashMap;

public class Demo05Test {
public static void main(String[] args) throws IOException {
// 1. 创建一个集合用来保存读取的文本
HashMap<String,String> map = new HashMap<>();
// 2. 创建缓冲流
BufferedReader br = new BufferedReader(new FileReader("day17-code\\src\\cn\\perdant\\day17\\demo06\\a.txt")); // 读取乱序文本所在的文件
BufferedWriter bw = new BufferedWriter(new FileWriter("day17-code\\src\\cn\\perdant\\day17\\demo06\\b.txt")); // 保存排好序的文本的文件

// 3. 读取文本,key存前面的序号;value存其余部分
String line;
while((line = br.readLine()) != null){ // 通过newLine可以一行一行读取
// split方法可以将一个字符串按照参数分成几个字符串并存在一个字符串数组中 每一行只有一个点,点前面是序号放到数组的第一个位置,剩下的放到数组第二个位置
String[] arr = line.split("\\.");
// 把序号做key 其余做value 放到集合中
map.put(arr[0],arr[1]); // key会根据序号自动排序
}

// 4. 遍历集合,重新组合写出
for (String key : map.keySet()) {
String value = map.get(key);
line = key+"."+value;
bw.write(line);
// 没写完集合中一条元素,记得换行
bw.newLine();
}

// 5. 释放资源
bw.close();
br.close();
}
}

转换流

字符编码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照A规则存储,同样按照A规则解析,那么就能显示正确的文本符号。反之,按照A规则存储,再按照B规则解析,就会导致乱码现象。

编码:字符(能看懂的)--字节(看不懂的)

解码:字节(看不懂的)-->字符(能看懂的)

字符编码Character Encoding : 就是一套自然语言的字符与二进制数之间的对应规则。

编码表:生活中文字和计算机中二进制的对应规则

字符集

字符集 Charset:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

计算机要准确的存储和识别各种字符集符号,需要进行字符编码,一套字符集必然至少有一套字符编码。常见字符集有ASCII字符集、GBK字符集、Unicode字符集等。

当指定了编码,它所对应的字符集自然就指定了,所以编码才是我们最终要关心的。

  • ASCII字符集
    • ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
    • 基本的ASCII字符集,使用7位(bits)表示一个字符,共128字符。ASCII的扩展字符集使用8位(bits)表示一个字符,共256字符,方便支持欧洲常用字符。
  • ISO-8859-1字符集
    • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰、丹麦、德语、意大利语、西班牙语等。
    • ISO-8859-1使用单字节编码,兼容ASCII编码。
  • GBxxx字符集
    • GB就是国标的意思,是为了显示中文而设计的一套字符集。
    • GB2312:简体中文码表。一个小于127的字符的意义与原来相同。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的"全角"字符,而原来在127号以下的那些就叫"半角"字符了。
    • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
    • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。
  • Unicode字符集
    • Unicode编码系统为表达任意语言的任意字符而设计,是业界的一种标准,也称为统一码、标准万国码。
    • 它最多使用4个字节的数字来表达每个字母、符号,或者文字。有三种编码方案,UTF-8、UTF-16和UTF-32。最为常用的UTF-8编码。
    • UTF-8编码,可以用来表示Unicode标准中任何字符,它是电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。它使用一至四个字节为每个字符编码,编码规则:
      1. 128个US-ASCII字符,只需一个字节编码。
      2. 拉丁文等字符,需要二个字节编码。
      3. 大部分常用字(含中文),使用三个字节编码。
      4. 其他极少使用的Unicode辅助字符,使用四字节编码。

编码引出的问题

在IDEA中,使用FileReader 读取项目中的文本文件。由于IDEA的设置,项目中创建的文件都是默认的UTF-8编码,所以没有任何问题。但是,当读取系统中创建的文本文件,如果我们创建文件时选择的编码是GBK(也叫做ANSI)编码,就会出现乱码。

OutputStreamWriter

转换流java.io.OutputStreamWriter extends Writer ,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造方法

  • OutputStreamWriter(OutputStream out)
  • OutputStreamWriter(OutputStream out, String charsetName)

参数: - OutputStream out:字节输出流 - String charsetName:指定的编码表名称,不区分大小写

步骤: 1. 创建OutputStreamWriter对象 2. 使用OutputStreamWriter对象的write方法 3. 使用OutputStreamWriter对象的flush方法 4. 释放资源

指定编码写出

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.*;

public class Demo01OutputStreamWriter {
public static void main(String[] args) throws IOException {
// OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day17-code\\src\\cn\\perdant\\day17\\demo07\\utf-8.txt"), "utf-8"); // 指定编码集 utf-8
// OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day17-code\\src\\cn\\perdant\\day17\\demo07\\utf-8.txt")); // 不写的话也默认是 utf-8
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("day17-code\\src\\cn\\perdant\\day17\\demo07\\gbk.txt"),"gbk"); // 指定编码集 gbk
osw.write("你好");
osw.flush();
osw.close();
}
}

InputStreamReader

java.io.InputStreamReader extends Reader,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受IDE的默认字符集。

构造方法

  • InputStreamReader(InputStream in)
  • InputStreamReader(InputStream in, String charsetName)

注意事项:构造方法中指定的编码表要和文件的编码相同,否则会发生乱码

指定编码读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class Demo02InputStreamReader {
public static void main(String[] args) throws IOException {
// InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\utf-8.txt")); // 默认是utf-8编码
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\gbk.txt"),"gbk"); // 指定gbk编码
int len = 0;
while((len = isr.read())!= -1){
System.out.print((char)len);
}
isr.close();
}
}

转换文件编码

将GBK编码的文本文件,转换为UTF-8编码的文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.io.*;

public class Demo03Practice {
public static void main(String[] args) throws IOException {
InputStreamReader isr = new InputStreamReader(new FileInputStream("D:\\gbk.txt"), "gbk");
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("D:\\utf-8.txt"),"utf-8");
int len = 0;
while ((len = isr.read()) != -1) {
osw.write(len);
}
osw.flush();
osw.close();
isr.close();
}

关于转换流的总结

其实你可以把InputStreamReader理解成FileReaderplus版本。对比一下二者

  • FileReader:读取文件,把文件内的字节以字节为单位传给FileReaderFileReader按照默认格式编码成字符,以字符为单位传给内存
  • InputStreamReader:读取文件,把文件内的字节以字节为单位传给InputStreamReaderInputStreamReader按照指定格式编码成字符,以字符为单位传给内存

其实他们二者底层都依靠了FileInputStream先来读取文件内的字节,但二者在将字节转换为字符这步有所不同。

我们再对比InputStreamWriterFileWriter

  • FileWriter:写出文件,把内存中的字符传给FileWriterFileWriter按照默认格式解码成字节。以字节为单位传给文件
  • InputStreamWriter:写出文件,把内存中的字符传给InputStreamWriterInputStreamWriter按照指定格式解码成字节。以字节为单位传给文件

同理,这二者都底层都依靠了FileOutStream

当然,InputStreamReader底层不单单可以使用 FileInputStream,任何InputStream的子类都可以,所以,FileReader仅仅是InputStreamReader构造时传入FileInputStream默认编码格式的一种特例。这就是为什么FileReader extends InputStreamReader

序列化

概述

我能不能把对象保存到文件中?不同于普通的写文件读文件,将对象写入文件以后,我们使用特定的读对象方法,返回的既不是字节也不是字符,而是一个对象Object。这样我们就可以直接使用这个对象的各种成员方法和变量

写对象

Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据对象的类型对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。

读对象

反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化对象的数据对象的类型对象中存储的数据信息,都可以用来在内存中创建对象。

ObjectOutputStream

java.io.ObjectOutputStream extends OutputStream 对象的序列化流 把对象以流的方式写入到文件中保存

构造方法

public ObjectOutputStream(OutputStream out)

特有的成员方法

void writeObject(Object obj)

使用步骤 1. 创建ObjectOutputStream对象,构造方法中传递字节输出流 2. 使用ObjectOutputStream对象中的方法writeObject把对象写入到文件中 3. 释放资源

序列化操作

  • 一个对象要想序列化,必须满足两个条件:
  1. 该类必须实现java.io.Serializable 接口,Serializable 是一个标记接口,当进行序列化和反序列化的时候,会检测类上是否又这个标记,有就可以序列化和反序列化,没有就会抛出NotSerializableException
  2. 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用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
28
29
30
31
32
33
34
35
36
37
38
import java.io.Serializable;

public class Person implements Serializable { // 想要序列化的类必须实现Serializable接口
private String name;
private int age;

public Person() {
}

public Person(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 "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
1
2
3
4
5
6
7
8
9
10
11
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class Demo01ObjectOutputStream {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo08\\Person.txt"));
oos.writeObject(new Person("白金之星",25)); // 把一个Person对象写入到Person.txt了
oos.close();
}
}

ObjectInputStream类

ObjectInputStream对象的反序列化流,把文件中保存的对象,以流的方式读取出来使用。

构造方法

public ObjectInputStream(InputStream in)

特有方法

public final Object readObject()

使用步骤 1. 创建ObjectInputStream对象 2. 使用ObjectInputStream对象中的方法readObject 3. 释放资源 4. 使用读取出来的对象(可以直接打印看看)

反序列化操作

  • 一个对象要想反序列化,必须满足两个条件:
  1. 类必须实现Serializable接口
  2. 对于JVM可以反序列化对象,它必须是能够找到class文件的类。如果找不到该类的class文件,readObject方法会抛出一个 ClassNotFoundException 异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Demo02ObjectInputStream {
public static void main(String[] args) throws IOException, ClassNotFoundException { // 除了IO异常,还有一个找不到Class文件异常
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("day17-code\\src\\cn\\perdant\\day17\\demo08\\Person.txt"));
Object o = ois.readObject();
ois.close();
Person o1 = (Person) o; // 把读取的对象类型强转为Person
System.out.println(o1.getName()+" "+o1.getAge()); // 可以直接调用对象的成员方法
}
}

反序列化遇到的异常

另外,当JVM反序列化对象时,能找到class文件,但是class文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个InvalidClassException异常。

缕一缕上面的过程:

  1. 先对Person类的实例做一个序列化写入Person.txt文件
  2. 修改Person
  3. Person.txt文件做一个反序列化
  4. 抛出异常InvalidClassException

分析产生异常的原因:

  1. 当定义好Person类之后,Person.java文件会通过javac编译成一个Person.class文件,这时Serializable 接口给需要序列化的类,提供了一个序列版本号。Person.class会有自己特有的一个序列版本号

  2. Person类实例序列化写入Person.txt文件之后,Person.txt会生成一个和Person.class相同的序列版本号

  3. 修改Person类之后,会重新编译新的Person.class文件。序列版本号也随之改变 > 并不是所有的修改都会让版本序列号改变。我简单的试了一下,修改类中的某些字符串内容不会导致版本号改变,但是修改成员变量的访问权限会导致版本号改变。

  4. Person.txt做反序列化时,会比较Person.txtPerson.class(已经改变了)文件的序列版本号是否相同,若相同,可以进行反序列化,若不同,抛出异常。

如何解决此问题:

  • 我只要让修改后的Person.class文件不会改变序列版本号即可。那我们自己给它指定一个不可变的序列版本号可以吗?
  • 在定义类的时候,我们添加一个成员变量 serialVersionUID并把它用static final修饰。
1
2
3
4
5
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// private int age;
public int age; // 无论我怎么修改,都可以成功的反序列化

关于静态 static

静态优先于非静态加载到内存中(静态优先于对象进入到内存中) 所以被static修饰的成员变量不能被序列化,会保留它的默认值。序列化的都是对象。

关于瞬态 transient

transient修饰的成员变量,不能被序列化, 它的作用就是,当你不想序列化对象中的某个特定的成员变量时,使用transient修饰它。

transientstatic修饰的成员变量都是不能序列化,但是在解决上面的版本号不一致问题时,不能用transient只能用static,说明二者还是有区别的。

详细讨论static与transient的区别

结合代码来分析二者区别:

1
2
3
4
5
6
7
8
9
//简单的整个JOJO类
import java.io.Serializable;

public class JOJO implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
private String standName;
...构造方法,set get toString都省略不贴在这里了
}
1
2
3
4
5
6
7
8
9
// 序列化
import java.io.*;

public class TestStaticTransient1 {
public static void main(String[] args) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test-code\\src\\cn\\perdant\\test\\demo03\\JoJo.txt"));
oos.writeObject(new Jojo("承太郎","白金之星"));
oos.close();
}
1
2
3
4
5
6
7
8
9
10
11
// 反序列化
import java.io.*;

public class TestStaticTransient2 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test-code\\src\\cn\\perdant\\test\\demo03\\JoJo.txt"));
Object obj = ois.readObject();
Jojo jojo = (Jojo) obj;
System.out.println(jojo);
}
}

先用static修饰

1
2
3
4
5
6
public class JOJO implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
private static String standName = "GoldExperience";
...
}

通过上述序列化反序列化后结果:JOJO{name='承太郎', standName='GoldExperience'}

static是类的变量,不是对象的,所以在序列化的时候,不会和序列化的变量一起写入文件,但此变量已经早于序列化和反序列化被加载到内存中去了,所以在反序列化的时候,静态变量仍存在,不过是类定义是的初始赋值。

个人见解:

这里再仔细讲一下,我在写demo的时候发现,如果我把序列化和反序列化的代码写道一起,在一起运行的时候,得到的结果是JOJO{name='承太郎', standName='白金之星'}

这是因为,如果你是分开写的,那么在序列化时,static修饰的变量没有序列化,而是在序列化开始之前就加载到内存的方法区中'GoldExperience',但是在序列化时调用了构造方法,并且构造方法中将静态变量修改成'白金之星',方法区中的静态变量值已经变了。但是你是分开写的,所以这个程序运行完,内存也已经释放了。再看反序列化

反序列化时,首先重新加载JOJO类,内存的方法区中的static修饰的变量仍是类定义的时候赋予的值'GoldExperience',然后进行反序列化,读对象,对象中的static修饰的变量值是不在文件中的,它是在内存的方法区中的,所以去方法区中找,找到了'GoldExperience'

如果把上面序列化和反序列化写在一起的话,可以看到,内存不会有释放重新加载这一步,那么内存方法区里的'GoldExperience'在序列化的时候就被修改成了'白金之星',而反序列化的时候,再去内存方法区找静态变量调用的时候,得到的就是'白金之星'

再看用transient修饰的例子

1
2
3
4
5
6
public class JOJO implements Serializable {
private static final long serialVersionUID = 42L;
private String name;
private transient String standName = "GoldExperience";
...
}

通过上述序列化反序列化后结果:JOJO{name='承太郎', standName='null'}

transient修饰的变量不会序列化,是完全不会写入文件又因为它不是静态变量也不会写入方法区,相当于此变量从来未被赋值,所以调用此变量时返回的是默认值null

练习:序列化集合

  1. 将存有多个自定义对象的集合序列化操作,保存到list.txt文件中。
  2. 反序列化list.txt ,并遍历集合,打印对象信息。

代码实现:

1
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
import java.io.*;
import java.util.ArrayList;

public class Demo01practice {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// 1. 定义一个存储Person对象的ArrayList集合
ArrayList<Person> list = new ArrayList<>();
//2. 往ArrayList集合中存储Person对象
list.add(new Person("马里奥", 35));
list.add(new Person("林克", 22));
list.add(new Person("塞尔达", 20));
//3. 创建一个序列化流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo08\\List.txt"));
// 4. 对集合序列化
oos.writeObject(list);
// 5. 创一个反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo08\\List.txt"));
// 6. 对集合反序列化
Object objList = ois.readObject();
// 7. 把Object类型集合转换为ArrayList类型
ArrayList<Person> arrayList = (ArrayList<Person>)objList;
// 8. 遍历集合
for (Person p : arrayList) {
System.out.println(p);
}
// 9. 释放资源
ois.close();
oos.close();
}
}

打印流

概述

平时我们在控制台打印输出,是调用print方法和println方法完成的,这两个方法都来自于java.io.PrintStream类,该类能够方便地打印各种数据类型的值,是一种便捷的输出方式。

PrintStream

java.io.PrintStream extends OutputStream (字节)打印流

PrintStream 为其他输出流添加了功能,使他们能够方便地打印各种数据值表示形式

PrintStream特点:

  1. 只负责数据的输出,不负责数据的读取
  2. 永远不会抛出IOException 但是会抛出 FileNotFoundException
  3. 有特有的方法,print,println,参数可以是任意类型的值

构造方法

public PrintStream(String fileName) public PrintStream(OutputStream out) public PrintStream(File file)

注意:

  • 如果使用继承父类的write方法写数据,那么查看数据的时候会查询编码表 97 - a
  • 如果使用自己特有的print,println方法写数据,写的数据原样输出 97 - 97
1
2
3
4
5
6
7
8
9
10
11
12
import java.io.FileNotFoundException;
import java.io.PrintStream;

public class Demo01PrintStream {
public static void main(String[] args) throws FileNotFoundException {
PrintStream ps = new PrintStream("D:\\IdeaProjects\\basic-code\\day17-code\\src\\cn\\perdant\\day17\\demo09\\Print.txt");
ps.write(97); // a
ps.println(97); // 97
ps.println("欧拉"); // 欧拉
ps.close();
}
}

改变打印流向

System.out就是PrintStream类型的,只不过它的流向是系统规定的,打印在控制台上。不过,既然是流对象,我们就可以玩一个"小把戏",改变它的流向。

使用System.setOut方法改变输出语句的目的地改为参数中传递的打印流的目的地 static void setOut(PrintStream out) 重新分配“标准”输出流

1
2
3
4
5
6
7
8
9
10
11
import java.io.FileNotFoundException;
import java.io.PrintStream;

public class Demo02PrintStream {
public static void main(String[] args) throws FileNotFoundException {
System.out.println("控制台输出");
PrintStream ps = new PrintStream("day17-code\\src\\cn\\perdant\\day17\\demo09\\Print.txt");
System.setOut(ps); // 把输出语句的目的地改变为打印流的目的地
System.out.println("Print.txt输出");
}
}

上述记录于2019.11.23

网络编程入门

软件结构

  • C/S结构 :全称为Client/Server结构,是指客户端和服务器结构。常见程序有QQ、迅雷等软件。

  • B/S结构 :全称为Browser/Server结构,是指浏览器和服务器结构。常见浏览器有谷歌、火狐等。

两种架构各有优势,但是无论哪种架构,都离不开网络的支持。网络编程,就是在一定的协议下,实现两台计算机的通信的程序。

网络通信协议

  • 网络通信协议:通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。

  • TCP/IP协议: 传输控制协议/因特网互联协议( Transmission Control Protocol/Internet Protocol),是Internet最基本、最广泛的协议。它定义了计算机如何连入因特网,以及数据如何在它们之间传输的标准。它的内部包含一系列的用于处理数据通信的协议,并采用了4层的分层模型,每一层都呼叫它的下一层所提供的协议来完成自己的需求。

TCP/IP协议中的四层分别是应用层、传输层、网络层和链路层,每层分别负责不同的通信功能。

  • 链路层:链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如针对光纤、网线提供的驱动。
  • 网络层:网络层是整个TCP/IP协议的核心,它主要用于将传输的数据进行分组,将分组数据发送到目标计算机或者网络。
  • 运输层:主要使网络程序进行通信,在进行网络通信时,可以采用TCP协议,也可以采用UDP协议。
  • 应用层:主要负责应用程序的协议,例如HTTP协议、FTP协议等。

协议分类

通信的协议还是比较复杂的,java.net 包中包含的类和接口,它们提供低层次的通信细节。我们可以直接使用这些类和接口,来专注于网络程序开发,而不用考虑通信的细节。

java.net 包中提供了两种常见的网络协议的支持:

  • UDP:用户数据报协议(User Datagram Protocol)。UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。

    • 由于使用UDP协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

    • 但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议。

    • 数据被限制在64kb以内,超出这个范围就不能发送了。(数据报(Datagram):网络传输的基本单位 )

  • TCP:传输控制协议 (Transmission Control Protocol)。TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。

    • TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。

    • 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠。

      • 第一次握手,客户端向服务器端发出连接请求,等待服务器确认。
      • 第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求。
      • 第三次握手,客户端再次向服务器端发送确认信息,确认连接。
    • 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛,例如下载文件、浏览网页等。

网络编程三要素

协议

  • 协议:计算机网络通信必须遵守的规则,已经介绍过了,不再赘述。

IP地址

  • IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。假如我们把“个人电脑”比作“一台电话”的话,那么“IP地址”就相当于“电话号码”。

IP地址分类

  • IPv4:是一个32位的二进制数,通常被分为4个字节,表示成a.b.c.d 的形式,例如192.168.65.100 。其中a、b、c、d都是0~255之间的十进制整数,那么最多可以表示42亿个。

  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。

为了扩大地址空间,拟通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,表示成ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。

常用命令

  • 查看本机IP地址,在控制台输入:
1
ipconfig
  • 检查网络是否连通,在控制台输入:
1
ping 空格 IP地址

特殊的IP地址

  • 本机IP地址:127.0.0.1localhost 这两个都代表当前计算机的ip

端口号

网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?

举个例子,电脑a上的qq连接电脑b上的qq,电脑a通过IP地址找到电脑b,但是b上除了qq还有很多其他软件比如飞秋、MSN等,如果我从电脑a上qq发送的消息传到了MSN上是没有任何意义的。那么我如何精确找到我想要传输到的进程呢?需要给每个进程分配一个端口,从而来区分各个进程。

如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。

  • 端口号:是一个逻辑端口,无法直接看到,可以使用一些软件查看端口号。当我们使用网络软件,一打开,操作系统就会为网络软件分配一个随机得端口号。或者,在打开软件的同时和系统要一个指定得端口号

  • 用两个字节表示的整数,它的取值范围是0~65535。其中,0~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败

  • 常见端口:

    1. 网络端口:80
    2. 数据库 mysql:3306 Oracle:1521
    3. Tomcat服务器:8080

利用协议+IP地址+端口号 三元组合,就可以标识网络中的进程了,那么进程间的通信就可以利用这个标识与其它进程进行交互。

TCP通信程序

概述

TCP通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。是面向连接的通信,客户端和服务器端必须经过3次握手,建立逻辑连接,才能通信安全。

两端通信时步骤:

  1. 服务器端先启动,服务器端不会主动的请求客户端,必须使用客户端请求服务器端
  2. 客户端和服务器端就会建立一个逻辑连接,而这个链接中包含一个对象,这个对象就是IO对象
  3. 客户端和服务器端就可以使用IO对象进行通信,通信数据不仅仅是字符,所以IO对象是字节流对象
  4. 客户端与服务器端进行一次数据交互,需要4个IO流对象

在Java中,提供了两个类用于实现TCP通信程序:

  1. 客户端:java.net.Socket 类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  2. 服务端:java.net.ServerSocket 类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。

服务器端必须明确两件事情: 1. 多个客户端同时和服务器进行交互,服务器必须明确和哪个客户端进行的交互 在服务器端有一个方法,叫accept客户端获取到请求的客户端对象Socket 2. 多个客户端同时和服务器进行交互,就需要使用多个IO流对象 服务器是没有IO流的,服务器可以获取到请求的客户端对象Socket,使用每个客户端Socket中提供的IO流和客户端进行交互: - 服务器使用客户端的字节输入流,读取客户端发送的数据 - 服务器使用客户端的字节输出流,给客户端回写数据 简而言之:服务器使用客户端的流和客户端交互

Socket类

java.net.Socket 类:该类实现客户端套接字

套接字指的是两台设备之间通讯的端点。个人理解:套接字就是 协议+IP地址+端口号 有了这三者,网络通信才能确定连接的两端。

构造方法

public Socket(String host, int port) :创建套接字对象并将其连接到指定主机上的指定端口号。如果指定的hostnull ,则相当于指定地址为回送地址。

参数: String host:服务器主机的名称/服务器的IP地址 int port:服务器的端口号

回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

成员方法

  • public InputStream getInputStream() : 返回此套接字的输入流。
    • 如果此Scoket具有相关联的通道,则生成的InputStream的所有操作也关联该通道。
    • 关闭生成的InputStream也将关闭相关的Socket
  • public OutputStream getOutputStream() : 返回此套接字的输出流。
    • 如果此Scoket具有相关联的通道,则生成的OutputStream的所有操作也关联该通道。
    • 关闭生成的OutputStream也将关闭相关的Socket
  • public void close() :关闭此套接字。
    • 一旦一个Socket被关闭,它不可再使用。
    • 关闭此socket也将关闭相关的InputStreamOutputStream
  • public void shutdownOutput() : 禁用此套接字的输出流。
    • 有时候我不想关闭整个Socket,我只想关闭Socket获取到的输出流。
    • 任何先前写出的数据将被发送,随后终止输出流。

客户端实现步骤

  1. 创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
  2. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
  3. 使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
  4. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
  5. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
  6. 释放资源(只需要关闭Socket即可)

注意:

  1. 客户端和服务器端进行交互,必须使用SocketgetXXXX提供的网络流,不要使用直接创建的流对象
  2. 当我们创建客户端对象Socket的时候,它就会去请求服务器和服务器经过3次握手,建立连接通路。这时,如果服务器没有启动,那么就会抛出ConnectException异常;如果服务器已经启动,那么就可以进行交互。

ServerSocket类

ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法

public ServerSocket(int port) :使用该构造方法在创建ServerSocket对象时,就可以将其绑定到一个指定的端口上

参数:int port:端口号,服务器端ServerSocket侦听该端口,一旦有客户端向该端口发送数据,服务器端就可以接收到

成员方法

服务器端只有一个,但是客户端可以有多个,所以服务器端必须明确一件事情,必须得知道是哪个客户端请求的服务器,所以首先使用accept方法获取到请求的客户端对象Socket

public Socket accept()侦听并接受连接,返回一个新的Socket对象,用于和客户端实现通信。该方法会一直阻塞直到建立连接。

所谓阻塞,就是既不继续向下运行,也不抛出异常终止程序,处在一个程序仍在运行但不向下运行的等待状态

服务器端实现步骤

  1. 创建服务器ServerSocket对象和系统要指定的端口
  2. 使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
  3. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
  4. 使用网络字节输入流InputStream对象中的方法read,读取客户端发送的数据
  5. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
  6. 使用网络字节输出流OutputStream对象中的方法write,给客户端回写数据
  7. 释放资源(ServerSocketSocket都要关闭)

简单的TCP网络程序

TCP通信分析

  1. 服务端启动,创建ServerSocket对象,等待连接。
  2. 客户端启动,创建Socket对象,请求连接。
  3. 服务端接收连接,调用accept方法,并返回一个Socket对象。
  4. 客户端Socket对象,获取OutputStream,向服务端写出数据。
  5. 服务端Scoket对象,获取InputStream,读取客户端发送的数据。

到此,客户端向服务端发送数据成功。

自此,服务端向客户端回写数据。

  1. 服务端Socket对象,获取OutputStream,向客户端回写数据。
  2. 客户端Scoket对象,获取InputStream,解析回写数据。
  3. 客户端释放资源,断开连接。
  4. 服务端释放资源(为了让服务器继续接收其他客户端发来的数据,通常不需要执行此步)

TCP通信实现

服务端实现:

1
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.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

/*
TCP通信的服务器端:接收客户端的请求,读取客户端发送的数据,给客户端回写数据
表示服务器的类:java.net.ServerSocket:此类实现服务器套接字
*/
public class TCPServer {
public static void main(String[] args) throws IOException {
// 1. 创建服务器ServerSocket对象和系统要指定的端口
ServerSocket server = new ServerSocket(8888);
// 2. 使用ServerSocket对象中的方法accept,获取到请求的客户端对象Socket
Socket socket = server.accept();
// 3. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 4. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
byte[] bytes = new byte[1024];
int len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 5. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 6. 使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
os.write("收到,谢谢".getBytes());
// 7. 释放资源(Socket,ServerSocket)
socket.close();
server.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
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

/*
TCP通信的客户端:向服务器发送连接请求,给服务器发送数据,读取服务器回写的数据
表示客户端的类:java.net.Socket
套接字:包含了IP地址和端口号的网络单位
*/
public class TCPClient {
public static void main(String[] args) throws IOException {
// 1. 创建一个客户端对象Socket,构造方法绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888); // 此ip是本机ip,端口随便写一个
// 2. 使用Socket对象中的方法getOutputStream()获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 3. 使用网络字节输出流OutputStream对象中的方法write,给服务器发送数据
os.write("你好服务器".getBytes());
// 4. 使用Socket对象中的方法getInputStream()获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 5. 使用网络字节输入流InputStream对象中的方法read,读取服务器回写的数据
int len = 0;
byte[] bytes = new byte[1024];
len = is.read(bytes);
System.out.println(new String(bytes,0,len));
// 6. 释放资源(Socket)
socket.close();
}
}

先运行服务器端,在运行客户端。服务器端的控制台打印:“你好服务器”,然后程序运行结束,客户端的控制台打印:“收到,谢谢” ,然后程序运行结束

综合案例

文件上传案例

客户端读取本地文件,把文件上传到服务器,服务器再把上传的文件保存到服务器的硬盘上

这里我们还是用之前IO流章节图片复制练习所使用的我老婆的图片,把我们本地计算机既当作客户端,又当作服务器端,要求读取C:\\我老婆.jpg,然后上传给服务器,服务器将图片保存到D:\\upload文件夹下

文件上传分析

  1. 客户端new本地字节输入流,从硬盘读取文件数据到程序中。
  2. 客户端get网络字节输出流,写出文件数据到服务端。
  3. 服务端get网络字节输入流,读取文件数据到服务端程序。
  4. 服务端new本地字节输出流,写出文件数据到服务器硬盘中。

至此已经完成:客户端读取本地文件,上传给服务器端,服务器端把文件写出到服务器文件

接下来是服务器向客户端写回“上传成功”信息

  1. 服务端get网络字节输出流,给客户端回写一个“上传成功”
  2. 客户端get网络字节输入流,读取服务端回写的数据
  3. 释放资源

注意事项:

  1. 客户端和服务器端和本地硬盘进行读写,需要使用自己创建的字节流对象(本地流)
  2. 客户端和服务器之间进行读写,必须使用Socket中提供的字节流对象(网络流)
  3. 文件上传的原理,就是文件的复制,明确数据源和数据的目的地

基本实现

服务端实现:

  1. 创建一个服务器ServerSocket对象,和系统要指定的端口号
  2. 使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
  3. 使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
  4. 判断D:\\upload文件夹是否存在,不存在则创建
  5. 创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
  6. 使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
  7. 使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器硬盘上
  8. 使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
  9. 使用网络字节输出流OutputStream对象的write方法,给客户端回写"上传成功"
  10. 释放资源(FileOutputStreamSocketServerSocket 为了继续侦听其他客户端发来的请求,一般不关闭ServerSocket
1
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
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class FileUploadServer {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务器ServerSocket对象,和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
// 2. 使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
Socket socket = server.accept();
// 3. 使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 4. 判断D:\\upload文件夹是否存在,不存在则创建
File file = new File("D:\\upload");
if (!file.exists()) {
file.mkdir();
}
// 5. 创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
FileOutputStream fos = new FileOutputStream(file+"\\我老婆.jpg");
int len = 0;
byte[] bytes = new byte[1024];
// 6. 使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
while((len = is.read(bytes)) != -1) {
// 7. 使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器硬盘上
fos.write(bytes,0,len);
}
// 8. 使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 9. 使用网络字节输出流OutputStream对象的write方法,给客户端回写"上传成功"
os.write("上传成功".getBytes());
// 10. 释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
server.close();
}
}

客户端实现:

  1. 创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
  2. 创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
  3. 使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
  4. 使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
  5. 使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
  6. 使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象
  7. 使用网络字节输入流InputStream对象中的方法read,读取服务器端回写的对象
  8. 释放资源(FileInputStream,Socket
1
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.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class FileUploadClient {
public static void main(String[] args) throws IOException {
// 1. 创建一个本地字节输入流FileInputStream对象,构造方法中绑定要读取的数据源
FileInputStream fis = new FileInputStream("C:\\我老婆.jpg");
// 2. 创建一个客户端Socket对象,构造方法中绑定服务器的IP地址和端口号
Socket socket = new Socket("127.0.0.1", 8888);
// 3. 使用Socket中的方法getOutputStream,获取网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 4. 使用本地字节输入流FileInputStream对象中的方法read,读取本地文件
int len = 0;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes))!= -1) {
// 5. 使用网络字节输出流OutputStream对象中的方法write,把读取到的文件上传到服务器
os.write(bytes,0,len);
}
// 写一个终止标记 这个很重要 下面会细讲
socket.shutdownOutput();
// 6. 使用Socket中的方法getInputStream,获取网络字节输入流InputStream对象
InputStream is = socket.getInputStream();
// 7. 使用网络字节输入流InputStream对象中的方法read,读取服务器端回写的对象
while((len = is.read(bytes))!= -1) {
System.out.println(new String(bytes,0,len));
}
// 8. 释放资源(FileInputStream,Socket)
fis.close();
socket.close();
}
}

注意:

这里我们要重点套路上面客户端实现中的第23行语句,如果不在这里加一个shutdownOutput(),我们会发现,文件已经写入到服务器的文件夹中了,但是客户端的控制台没有打印“上传成功”,而且服务器和客户端的程序都没有停止运行。这说明程序运行到某一处之后处于了阻塞状态。是哪里呢?下面我们从头捋一捋:

  1. 首先运行服务器端,服务器创建ServerSocket对象,处于侦听状态
  2. 然后运行客户端,创建FileInputStream对象和Socket对象中的方法getOutputStream获取OutputStream对象
  3. 客户端使用FileInputStream对象读取图片,使用OutputStream对象将图片写出给服务器,采用的循环读取,终止条件是读取到结束符时终止,但是这样写入的字节是不包含终止符的
  4. 服务器端accept方法获取到请求的客户端Socket对象,并使用getInputStream获取InputStream对象
  5. 服务器端使用InputStream对象读取客户端传来的字节,使用FileOutputStream对象写出到服务器文件
  6. 同样是采用与第3步相同的循环读取,但是从客户端传来的字节是不含终止符的,所以读取的循环是一直不会终止的,所以在这里会处于阻塞状态,程序一直循环读取,一直循环写出,而后面的代码无法继续执行

因为程序一直在读取写入,所以程序结果会变成图片上传复制成功但程序没有终止,而且后续的服务器写回客户端的信息也没有打印,为了解决这个问题,我们需要使用shutdownOutput()方法,这个方法可以关闭SocketOuputStream流,并且在写出的字节最后加一个终止符。这样服务器端的循环读取就会在读取到结束符时停止读取。让程序可以继续向下进行。

文件上传优化分析

  1. 文件名称写死的问题

服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一

1
String fileName = "perdant"+ System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg"; // 一般格式是域名+时间+随机书+文件后缀

在网上下载下来的文件名一般都是很长一串数字,往往就是采用了这种命名方法,这样每次下载下来的相同的文件,名字却往往不一样

  1. 循环接收的问题

服务端,只保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件

1
2
3
4
5
6
7
8
9
10
11
12
public class FileUploadServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8888);
// 让服务器一直处于监听状态 写一个死循环 有一个客户端上传文件,就保存一个文件。
while(true) {
Socket socket = server.accept();
... // 中间和之前的代码一样 省略不写了
socket.close();
}
// server.close(); 服务器一直是监听状态,不需要关闭
}
}
  1. 效率问题

服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化

1
2
3
4
5
6
7
8
9
whiletrue){
Socket accept = serverSocket.accept();
// accept 交给子线程处理.
new Thread(() -> {
......
InputStream bis = accept.getInputStream();
......
}).start();
}

我们把上面的所有优化(都是针对服务器端的)放入之前的代码中

1
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
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Random;

public class FileUploadServer {
public static void main(String[] args) throws IOException {
// 1. 创建一个服务器ServerSocket对象,和系统要指定的端口号
ServerSocket server = new ServerSocket(8888);
// 2. 使用ServerSocket对象中的方法accept,获取到请求的客户端Socket对象
// 让服务器一直处于监听状态 写一个死循环 有一个客户端上传文件,就保存一个文件。
while(true) {
Socket socket = server.accept();
// 使用多线程技术,有一个客户端上传文件,就开启一个线程,完成文件上传
// 3. 使用Socket对象中的方法getInputStream,获取到网络字节输入流InputStream对象
new Thread(new Runnable() {
@Override
public void run() { // run方法不能声明抛出异常,只能try catch
// 重写run方法,用来完成文件的上传
try{
InputStream is = socket.getInputStream();
// 4. 判断d:\\upload文件夹是否存在,不存在则创建
File file = new File("d:\\upload");
if (!file.exists()) {
file.mkdir();
}
// 5. 创建一个本地字节输出流FileOutputStream对象,构造方法中绑定要输出的目的地
// 自定义文件名,每次运行都会得到不同的文件名
String fileName = "perdant"+ System.currentTimeMillis() + new Random().nextInt(999999) + ".jpg";
FileOutputStream fos = new FileOutputStream(file+"\\"+fileName);
int len = 0;
byte[] bytes = new byte[1024];
// 6. 使用网络字节输入流InputStream对象中的方法read,读取客户端上传的文件
while((len = is.read(bytes)) != -1) {
// 7. 使用本地字节输出流FileOutputStream对象中的方法write,把读取到的文件保存到服务器硬盘上
fos.write(bytes,0,len);
}
// 8. 使用Socket对象中的方法getOutputStream,获取到网络字节输出流OutputStream对象
OutputStream os = socket.getOutputStream();
// 9. 使用网络字节输出流OutputStream对象的write方法,给客户端回写"上传成功"
os.write("上传成功".getBytes());
// 10. 释放资源(FileOutputStream,Socket,ServerSocket)
fos.close();
socket.close();
}catch (IOException e){
System.out.println(e);
}
}
}).start();
}
// server.close();
}
}

模拟B/S服务器(扩展知识点)

浏览器端服务器端分析

在网络编程章节一开始,我们就讲过,除了C/S结构,还有一种B/S结构,就是浏览器与服务器连接的结构,和C/S结构类似,只不过把客户端换成了浏览器,而之间交互的数据往往是html文件。下面我来简单的捋一捋过程 然后写一个demo,因为里面用到了html文件,所以这里的代码你们可以自己先找一个html文件,再来复现

  1. 浏览器里输入一个地址
  2. 服务器接收到浏览器发来的请求,根据请求的内容,解析出浏览器想要访问的文件的地址
  3. 服务器读取文件并向浏览器回写(一般不止一个html文件,我用一个web文件夹储存了所有与网页有关的文件)
  4. 浏览器中可以把这个html文件以网页的形式显示出来

我们之前的客户端就是现在的浏览器,相比C/SB/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
49
50
51
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
public static void main(String[] args) throws IOException {
// 创建一个服务器SeverSocket对象,端口号和你在浏览器中输入的地址有关
// 浏览器里地址格式 协议 + 服务器IP地址 + 端口号 + html文件在服务器的路径
// 我是本机模拟服务器 浏览器里输入:http://127.0.0.1:8888/day18-code/src/cn/perdant/day18/demo04/web/index.html
// 我的html文件时存在项目里面的,所以用相对路径即可
ServerSocket server = new ServerSocket(8888);
// 网页上的图片不是写在html文件里面,而是单独存储在与html同目录(web文件夹)下的,当浏览器读取index.html时,遇到html上有图片,会再去找到图片的存储位置,浏览器会创建一个新线程请求服务器,服务器再写回图片,浏览器读取图片
while(true){
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try{
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
// 浏览器一开始读取到的并不直接是html文件,而是一个包含html文件地址的信息,需要把html文件的地址提取出来,通过字符串的分隔、取子字符串等方法取得html文件的地址
String line = br.readLine();
String[] arr = line.split(" ");
String htmlpath = arr[1].substring(1);
// 根据html地址创建输入流,从服务器硬盘上读取到服务器内存里,再通过网络输出流写回给浏览器,在写之前,需要在开头先写一些固定格式的字符串。
FileInputStream fis = new FileInputStream(htmlpath);
OutputStream os = socket.getOutputStream();
// 写入HTTP协议响应头,固定写法,学到现在我还不知道这个具体原理,暂且死记
os.write("HTTP/1.1 200 OK\r\n".getBytes());
os.write("Content-Type:text/html\r\n".getBytes());
// 必须要写入空行,否则浏览器不解析
os.write("\r\n".getBytes());
// 服务器本地流读取html文件的同时,网络流写出给浏览器
int len = 0;
byte[] bytes = new byte[1024];
while((len = fis.read(bytes)) != -1) {
os.write(bytes,0,len);
}
// 释放资源
fis.close();
socket.close();
}catch(IOException e){
System.out.println(e);
}

}
}).start();
}
// server.close(); 不需要关闭 让服务器一直处于监听状态
}
}

上述记录于2019.11.26