参考 B 站楠哥教你学 Java2021 最新 Java 零基础实战全套教程,从入门到精通视频

Java 体系

Java 基础

1、跨平台(在各个平台、系统都能运行)

2、面向对象编程语言(Java,C++)

3、分布式计算

Java 的运行机制

  1. 编写 Java 程序
  2. 编译 Java 文件
  3. JVM 读取字节码文件运行程序

Java 的三大体系

  • Java SE(Java Platform,Standard Edition)
  • Java ME(Java Platform,Enterprise Edition)
  • Java EE(Java Platform,Micro Edition)

配置 Java 环境

JRE:Java Runtime Environment,Java 运行环境

JDK:Java Development Kit,Java 开发工具包

开发

编译

1
javac HelloWorld.java

运行

1
java HelloWorld

Java IDE,集成开发环境,就是说开发 Java 的工具:NetBeans、Eclipse、IDEA

代码规范

强制性代码规范(必须执行的)

  1. Java 程序的文件名类名必须一致,若不一致,无法通过编译。

  2. main 方法是程序的入口,方法的定义必须严格按照格式书写。

  3. 类是组织 Java 代码结构的,类中的方法是执行具体业务的。

非强制性代码规范(建议按照此方式编写代码)

  1. 一行只写一条语句。

  2. 在 1 的基础上,还要注意代码缩进。


基本概念

安装 Java 环境

  • 编写 Java 代码

  • 编译 Java 代码,成为字节码文件 16 进制

    javac 文件名(带后缀)

  • 让 JVM 执行字节码文件,运行程序

    java 文件名(不带后缀)

注释

注释就是用通俗易懂的语言对代码进行描述解释,方便自己和他人阅读。

单行注释

1
//注释内容

多行注释

1
2
3
4
/*注释内容
注释内容
注释内容
*/

文档注释

1
2
3
4
/**注释内容
*注释内容
*注释内容
*/

关键字

Java 语言预先定义好的,有指定意义的标识符,组成程序的基本元素。

关键词解释
abstract表示抽象
boolean基本数据类型
break跳出循环
byte基本数据类型
case与 switch 搭配使用
catch与 try 搭配使用,表示捕获异常
char基本数据类型
class表示一个类
continue跳出循环
do与 while 搭配使用,表示循环
double基本数据类型
else与 if 搭配,流程控制
enum枚举类型
extends继承
final修饰常量
float基本数据类型
if流程控制
implements实现接口
import引入某个类
int基本数据类型
interface表示接口
long基本数据类型
native表示本地方法
new创建对象
package表示包
private私有
public共有
return返回值
short基本数据类型
static表示静态
super表示父类
switch与 case 搭配使用
synchronized线程同步
this表示当前实例
throw抛出异常
throws方法中主动抛出异常
try与 catch 搭配使用
void表示方法没有返回值
volatile保证线程读取到最新值
while表示循环

变量

  • 数据类型
  • 变量名
  • 变量值

基本数据类型 + 引用数据类型

使用变量

  1. 声明变量的数据类型和变量名(包含数字、字母、下划线、$,不能包含空格、运算符,不能用关键字命名,不能以数字开头),大小写可以混用,首单词应该小写,后续单词的首字母大写。

    userId、studentName (驼峰式命名法)

  2. 给内存空间赋值,该值就是变量值。

Java 的数据类型

基本数据类型

byte、int、short、long、float、double、char、boolean

数值类型(整数、小数)

基本数据类型字节
byte1 个字节 (8 位)
int4 个字节(32 位)
short2 个字节(16 位)
long8 个字节(64 位)
float4 个字节(32 位)单精度浮点型
double8 个字节(64 位)双精度浮点型

非数值类型(文本)

基本数据类型字节
char字符 2 个字节(16 位)
boolean1/8 个字节、判断逻辑是否成立 true 1 / false 0

引用数据类型

数据类型转换

自动转换

Java 可以自动对某些数据类型进行自动转换。

规则:只能由低字节向高字节进行转换,反之则不行。

byte👉short👉int👉long👉float👉double

强制类型转换

Java 无法自动转换的数据类型,开发者可以通过强制手段进行转换。

一般来讲强制类型转换可能会造成精度损失。

1
2
double num = 10.0;
int num2 = (int)num;

运算符

赋值运算符

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

1
2
3
4
5
6
7
8
9
10
//1、创建变量用来记录张三的体重
double weight1 = 70.5;
//2、创建变量表示李四的体重
double weight2 = 60.5;
System.out.println("交换之前:张三的体重是"+weight1+",李四的体重是"+weight2);
System.out.println("进行交换");
double temp = weight1;
weight1 = weight2;
weight2 = temp;
System.out.println("交换之后:张三的体重是"+weight1+",李四的体重是"+weight2);

算术运算符

  • 基本算术运算符

    +、-、*、/、%、++、--

    A + B

    A - B

    A * B

    A / B

    A % B

    变量++、++变量

    变量--、--变量

    变量++:先操作,再运算。

    ++变量:先运算,再操作。

  • 复合算术运算符

    +=、-=、*=、/=、%=

    A += B:先求出 A 和 B 之和,再把计算结果赋值给 A,作用相当于 A = A + B

  • 关系运算符

    ==、!=、>、<、>=、<=

  • 逻辑运算符

    逻辑运算符只能用于 boolean 类型的数据运算,判断 boolean 数据之间的逻辑关系,与、或、非。

    &(与)、|(或)、!(非)、&&(短路与)、||(短路或)

    参与逻辑运算符的变量都是 boolean 的

    A & B:只有当 A 和 B 都为 true,结果为 true,否则为 false。

    A | B:A 和 B 只要有一个为 true,结果为 true,否则为 false。

    !A:若 A 为 true,结果为 false,若 A 为 false,结果为 true。

    A && B:只有当 A 和 B 都为 true,结果为 true,否则为 false。

    A || B:A 和 B 只要有一个为 true,结果为 true,否则为 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int num1 = 10;
int num2 = 11;
System.out.println((++num1==num2)||(num1++==num2));
System.out.println(num1);

int num1 = 10;
int num2 = 11;
System.out.println((++num1==num2)|(num1++==num2));
System.out.println(num1);

int num1 = 10;
int num2 = 11;
System.out.println((num1++==num2)&(++num1==num2));
System.out.println(num1);

int num1 = 10;
int num2 = 11;
System.out.println((num1++==num2)&&(++num1==num2));
System.out.println(num1);

条件运算符

三元运算符、三目运算符、三元表达式

根据不同的条件给同一个变量赋不同的值,变量 = 条件 ? 值1 : 值2.

位运算符

十进制和二进制的转换

十进制转二进制:目标数除以 2,若能除尽,该位记做 0,若除不尽,该位记做 1,再对商继续除以 2,以此类推,直到商为 0,然后把每一位的结果反序组合就是对应的二进制。

10:1010

17:10001

二进制转十进制:从目标数的最后侧起,本位的数值乘以本位的权重,权重就是 2 的第几位的位数减一次方,将每一位的值进行相加,得到的结果就是对应的十进制。

位运算符:&(按位与)、|(按位或)、^(按位异或)、<<(左移)、>>(右移)

A & B :先把 A 和 B 转为二进制,每一位的数字一一对应,进行比较判断,若都为 1,则该位记做 1,否则记做 0。

A | B : 先把 A 和 B 转为二进制,每一位的数字一一对应,进行比较判断,只要有一个为 1,则该位记做 1,否则记做 0。

A ^ B : 先把 A 和 B 转为二进制,每一位的数字一一对应,进行比较判断,相同记做 0,不同记做 1。

A << B : A 乘以 2 的 B 次方

2 << 3 : 2 * 8 = 16

A >> B : A 除以 2 的 B 次方

2 >> 3 : 2/8 = 0

运算符的优先级

! > 算术运算符 > 关系运算符 > 逻辑运算符(&&>||)


流程控制

选择流程控制

if else

用来判断某个条件是否成立,然后执行不同的逻辑运算。

基本语法:

1
2
3
4
5
if (判断条件) {
//条件成立的代码
}else{
//条件不成立的代码
}

多重 if

  • if 后面必须跟条件
  • else 后面不能跟条件
  • else 后面可以根{},也可以跟 if

循环流程控制

for、while、do-while、foreach

循环四要素:

  • 初始化循环变量
  • 循环条件
  • 循环体
  • 更新循环变量

switch-case

与 if 不同的是,switch-case 只能完成等值判断,而无法完成判断大小。

如果是判断两个值是否相等,可以使用 switch-case,如果比较两个值的大小关系,则不能使用 switch-case。

switch 支持 int、short、byte、char、枚举、String 类型,不支持 boolean 类型。

基本语法

1
2
3
4
5
6
7
8
9
10
11
12
switch (变量) {
case1:
//业务代码
break;
case2:
//业务代码
break;
...
default:
//业务代码
break;
}

case 判断变量是否等于某个值,default 表示所有的 case 都不成立的情况下所执行的代码。

  • 1 奖励 2000
  • 2 奖励 1000
  • 3 奖励 500
  • 否则没有奖励
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) {
int placing = 1;
if(placing == 1) {
System.out.println("奖励2000元");
}else {
if(placing == 2) {
System.out.println("奖励1000元");
}else {
if(placing == 3) {
System.out.println("奖励500元");
}else{
System.out.println("没有奖励");
}
}
}

switch(placing) {
case 1:
System.out.println("奖励2000元");
break;
case 2:
System.out.println("奖励1000元");
break;
case 3:
System.out.println("奖励500元");
break;
default:
System.out.println("没有奖励");
break;
}
}

while

1
2
3
4
5
初始化循环变量
while (循环条件) {
循环体
更新循环变量
}
1
2
3
4
5
6
7
8
9
//初始化循环变量
int num = 0;
//循环条件
while(num < 10) {
//循环体
System.out.println("Hello World");
//更新循环变量
num++;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int num = 0;
String flag = "y";
while(flag.equals("y")) {
System.out.print("请输入学生学号:");
Scanner scanner = new Scanner(System.in);
int id = scanner.nextInt();
switch(id) {
case 1:
System.out.println("张三的成绩是96");
break;
case 2:
System.out.println("李四的成绩是91");
break;
case 3:
System.out.println("王五的成绩是89");
break;
default:
System.out.println("请输入正确的学号");
break;
}
System.out.print("是否继续?y/n");
flag = scanner.next();
}
System.out.println("感谢使用学生成绩查询系统");

do-while

1
2
3
4
5
6
7
8
9
//初始化循环变量
int num = 0;
do {
//循环体
System.out.println("Hello World");
//更新循环变量
num++;
}while(num<10);
//循环条件
1
2
3
4
5
6
7
8
Scanner scanner = new Scanner(System.in);
String result = "";
do {
System.out.println("张三参加体能测试,跑1000米");
System.out.print("是否合格?y/n");
result = scanner.next();
} while (result.equals("n"));
System.out.println("合格,通过测试");

for

1
2
3
for (初始化循环变量;循环条件;更新循环变量) {
循环体
}
1
2
3
for (int num = 0; num < 10; num++) {
System.out.println("Hello World");
}

while、do-while、for 3 种循环的区别

相同点:都遵循循环四要素,初始化循环变量、循环条件、循环体、更新循环变量。

不同点

  • while 和 do-while 适用于循环次数不确定的业务场景;for 适用于循环次数确定的场景。
  • while 和 for 都是先判断循环条件,再执行循环体;do-while 先执行循环体,再判断循环条件。

分别使用 while、do-while、for 循环输出 10 以内的所有奇数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//while循环
int num = 0;
while(num <= 10) {
if(num%2!=0) {
System.out.println(num);
}
num++;
}
//do-while循环
int num = 0;
do {
if(num%2!=0) {
System.out.println(num);
}
num++;
}while(num <= 10);
//for循环
for(int num = 0;num <= 10;num++) {
if(num%2!=0) {
System.out.println(num);
}
}

for 循环只适用于循环次数确定的场景下(for 也可以适用于循环次数不确定的场景,只不过一般不会用这种方式进行开发),while 和 do-while 循环次数确定或者不确定都可以使用。

1
2
3
4
5
6
7
String result = "n";
for(;result.equals("n");) {
System.out.println("张三参加体能测试,跑1000米");
System.out.print("是否合格?y/n");
result = scanner.next();
}
System.out.println("合格,通过测试");

数组

数组

数组就是一种可以存储大量数据类型相同的变量的数据结构,数组就是一个具有相同数据类型的数据集合。

数组的基本要素

  • 数组名称
  • 数组元素
  • 元素下标
  • 数据类型

数组本身就是一个变量,数组名称就是变量名,数组中保存的每一个数据都会有一个下标(从 0 开始)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//声明数组
int[] array;
//分配内存空间
array = new int[6];
//给数组赋值
array[0] = 1;
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;
array[5] = 6;

int[] array2 = {1,2,3,4,5,6};
int[] array3 = new int[]{1,2,3,4,5,6};

int[] array4;
for (int i = 0; i < array4.length; i++) {
// TODO
}

数组常见的错误

  • 数据类型不匹配。
  • 边声明边赋值的方式,代码必须写在同一行,不能换行。
  • 数组下标越界。

数组的常用操作及方法

  • 求数组的最大值
  • 求数组的最小值
  • 在数组的指定位置插入数据
  • 对数组进行排序

二维数组

二维数组简单理解即一维数组中保存的值是另外一个一维数组。

变量、数据类型、流程控制、循环、数组。

用户管理系统

  • 查询用户:将系统中保存的全部用户信息在控制台打印输出。
  • 添加用户:向系统中添加新的用户信息,如果添加的用户已经存在,给出提示信息。
  • 删除用户:输入用户名,进行删除操作,若输入的用户名不存在,给出提示信息。
  • 账号冻结:输入用户名,进行冻结操作,若输入的用户名不存在或者该用户已经被冻结,给出相应提示。
  • 账号解冻:输入用户名,进行解封操作,若输入的用户名不存在或者该用户状态正常,给出相应提示。
  • 退出系统:跳出循环,给出提示信息。

面向对象程序

面向对象

面向对象编程思想:将程序模块化的思想。

什么是面向对象?

面向对象编程思想诞生之前,程序开发采用的是面向过程的结构化编程方式,是一种面向功能划分的软件结构。

最小粒度细化到方法这一层。

面向过程注重的是每一个步骤,面向对象关注点在于整件事情的模块化结构。

三大特征

封装、继承、多态

类和对象

类和对象的关系

每个对象都有特定的特征:1、属性。2、方法。

属性指的是对象的静态特征,方法用来描述对象的动态特征。

对象是用来描述客观存在的一个实体,该实体是由一组属性和方法构成。

类是与对象紧密结合的另外一个概念,类是产生对象的模版,所有的对象都是通过类来创建的。

二者的关系

  • 类是对象的抽象化描述,这些对象具有相同的特征和动作(属性和方法)。

  • 对象是类的具体实例。

Java 程序是以类为组织单元,程序运行时的主体是通过类创建的具体对象。

定义类

1
2
3
4
5
6
7
8
9
public class 类名{
// 定义属性,属性名符合驼峰式命名法
public 数据类型 属性名;

// 定义方法,方法名符合驼峰式命名法
public 返回值类型 方法名 (参数列表:数据类型 参数名) {
// 方法体
}
}

Java 关于返回值的定义分为两类:有返回值和无返回值,有返回值的方法需要在方法定义时指定返回值的数据类型,并在方法体中用 return 将结果返回给外部调用者。

如果一个方法不需要进行返回操作,将返回值类型定义为 void。

参数列表是指外部在调用该方法时需要传入到方法内部进行运算的数据。

构造函数、构造方法、构造器

构造函数是一种特殊的方法,普通方法是用来描述某个动作的,构造方法是用来创建对象的。

  • 方法名必须与类名一致。
  • 不需要定义返回值类型。

构造函数可分为有参构造和无参构造,有参构造是指带参数的构造函数,无参构造是指没有参数的构造函数。

任何一个类都默认自带一个无参构造函数,如果手动在类中定义一个有参构造,则会覆盖默认的无参构造。

this 关键字

this 用来指代当前类的实例化对象,通过 this 可以调用当前类的属性和方法,比如在有参构造中,通过 this 将外部传入的值赋给当前类的实例化对象。

this 除了可以在类中访问属性也可以在类中调用方法,类中的方法可以分为两类:构造方法、普通方法。

用 this 调用这两类方法的语法也不同,调用构造函数的语法是 this(参数列表),不能在普通方法中使用 this 调用构造函数。

用 this 调用普通方法,this.方法名(参数列表),可以在构造函数中使用,也可以在普通方法中使用。

成员变量和局部变量

变量的作用域是指在程序中可以通过变量名来访问该变量的范围,变量的作用域由变量被声明时所在位置决定的,Java 中根据不同的作用域可以将变量分为成员变量和局部变量。

局部变量:如果一个变量在方法中声明,则该变量是局部变量。

成员变量:如果一个变量在方法外,类中声明,则该变量是成员变量。

1
2
3
4
5
6
public class HelloWorld {
int num2 = 2;
public int test() {
int num1 = 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
29
30
31
32
33
public class Student {
private int id;
private String name;
private int age;
public void show() {
System.out.println("学生信息如下:");
System.out.println("学生编号:"+id);
System.out.println("学生姓名:"+name);
System.out.println("学生年龄:"+age);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if(age <= 0) {
System.out.println("输入的数值有误!");
age = 18;
}
this.age = age;
}
}

封装的核心思想就是尽可能把属性都隐藏在内部,对外提供方法来访问,我们可以在这些方法中添加逻辑处理来实现过滤,以屏蔽错误数据的赋值

封装的步骤

  • 修改属性(成员变量)的访问权限为私有 private,使得外部不能直接访问。
  • 提供外部可以直接调用的方法。
  • 在该方法中加入对于属性的逻辑控制,避免出现逻辑上的错误。

什么是访问权限?

访问权限是指该属性可以被直接访问的范围,是在属性定义时设定的,访问权限的可选项一共有 4 种:public、private、默认(不写)、protected,区别在于作用域范围不同。

static

static 表示静态或者全局,可以用来修饰成员变量和成员方法以及代码块。

使用 static 修饰的成员变量和成员方法独立于该类的任何一个实例化对象,访问时不依赖于该类的对象,而是直接通过类去访问,可以理解为被该类的所有实例对象所共用,所以说是全局的。

static 还可以修饰代码块,被 static 修饰的代码块叫做静态代码块。

1
2
3
static {
System.out.println(1);
}

静态代码块的特点是只执行一次,什么时候执行?当这个类被加载到内存时执行,不需要开发者手动调用,会自动执行。

被加载到内存中的类叫做运行时类,静态代码块就是在家中类的时候执行的,因为类只加载一次,所以静态代码块也只执行一次。

继承

什么是继承?

继承是用来描述类之间的关系的,即一个类继承(拥有)另外一个类中的属性和方法,被继承的类叫做父类,继承父类的类叫做子类。

继承的基本语法

1
2
3
public class 类名 extends 父类名{

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class People {
private int id;
private String name;
private int age;
private char gender;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
}
1
2
3
public class Student extends People {

}

Java 中的继承是单继承,也就是说一个子类只能有一个直接父类。

子类访问父类

创建一个子类对象的时候,会默认先创建一个父类对象,无论是通过有参构造或是无参构造来创建子类对象,都是通过无参构造来创建父类对象

可以通过 super 关键字让子类创建对象时调用父类的有参构造。

1
2
3
4
public Student() {
super(1);
System.out.println("通过无参构造创建了Student对象");
}

子类可以访问父类的构造方法、普通方法、成员变量,都是通过 super 关键字来完成,具体语法:

1
2
3
构造方法:super(参数列表)
普通方法:super.方法名(参数列表)
成员变量:super.成员变量名

在子类的构造方法中,可以通过 super 访问父类的构造方法和普通方法。

在子类的普通方法中,只能通过 super 访问父类的普通方法。

注意:super(); 放首行

子类的访问权限

访问权限修饰符:public、protected、默认修饰符、private

同一个类中不同包同包 | 子类(同包)子类(不同包)
public可以访问可以访问可以访问可以访问
protected可以访问不能访问可以访问可以访问
private可以访问不能访问不能访问不能访问
默认修饰符可以访问不能访问可以访问不能访问

:package,用来管理 Java 文件,一个项目中不可避免会出现同名的 Java 类,为了防止产生冲突,可以把同名的 Java 类分别放入不同的包中。

包的命名规范:包名由小写字母组成,不能以 . 开头或结尾,可以包含数字,但不能以数字开头,使用"."来分层。

包的命名方式一般采用网络域名的反向输出,如 com.company.test/com.company.entity

方法重写

方法重写:子类在继承父类方法的基础上,对父类方法重新定义并覆盖的操作

构造方法不能被重新,方法重写的规则:

  1. 父子类的方法名相同。

  2. 父子类的方法参数列表相同。

  3. 子类方法的返回值与父类方法返回值类型相同或者是其子类。

  4. 子类方法的访问权限不能小于父类。

方法重写 VS 方法重载

方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或个数

位置:方法重写在子类中对父类方法进行重写,方法重载是在同一个类中。

方法名:方法重写相同,方法重载相同。

参数列表:方法重写相同,方法重载不同。

返回值:方法重写相同或是其子类,方法重载没有要求。

访问权限:方法重写不能小于父类,方法重载没有要求。

多态

一个事物具有多种表现形态,在 Java 程序中,定义一个方法,在具体的生成环境中根据不同的需求呈现不同的业务逻辑,多态的前提是继承

1
2
3
4
5
6
7
package com.southwind.test;

public class Member {
public void buyBook() {

}
}
1
2
3
4
5
6
7
package com.southwind.test;

public class OrdinaryMember extends Member {
public void buyBook() {
System.out.println("普通会员买书打9折");
}
}
1
2
3
4
5
6
7
package com.southwind.test;

public class SuperMember extends Member {
public void buyBook() {
System.out.println("超级会员买书打6折");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.southwind.test;

public class Cashier {
private Member member;

public Member getMember() {
return member;
}

public void setMember(Member member) {
this.member = member;
}

public void settlement() {
this.member.buyBook();
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.southwind.test;

public class Test {
public static void main(String[] args) {
OrdinaryMember ordinaryMember = new OrdinaryMember();
SuperMember superMember = new SuperMember();
Cashier cashier = new Cashier();
cashier.setMember(superMember);
cashier.settlement();
}
}

多态的具体使用有两种形式:

  1. 定义方法时形参类型为父类,实际调用方法时传入子类类型的参数。

  2. 定义方法时返回值类型为父类,实际调用方法时返回子类对象。

以上两种形式的基本原理都是父类引用可以指向子类对象。

1
2
3
4
5
6
7
public void settlement(Member member) {
member.buyBook();
}

Cashier cashier = new Cashier();
OrdinaryMember ordinaryMember = new OrdinaryMember();
cashier.settlement(ordinaryMember);
1
2
3
4
5
6
7
public Member getMember(String name) {
if (name.equals("ordinary")) {
return new OrdinaryMember();
} else {
return new SuperMember();
}
}

抽象方法和抽象类

如果一个方法只有方法的声明而没有具体的方法实现,这个方法就叫做抽象方法,Java 中的抽象方法需要使用 abstract 关键字来修饰。

1
public abstract void buyBook();

一旦类中定义了抽象方法,则该类也必须声明为抽象类,需要在类定义处添加 abstract 关键字。

1
2
3
public abstract class Member {
public abstract void buyBook();
}

抽象类与普通类的区别是抽象类不能被实例化。

抽象方法与普通方法的区别是抽象方法没有方法体。

抽象类中可以没有抽象方法,但是包含了抽象方法的类必须定义为抽象类。即我们可以在抽象类中定义普通方法,但是在普通类中不能定义抽象方法。

如果父类是抽象类,一旦子类继承了该抽象父类,则子类必须对父类的抽象方法进行重写,否则程序报错。

1
2
3
4
5
package com.southwind.test;

public abstract class Member {
public abstract void buyBook();
}
1
2
3
4
5
6
7
8
9
package com.southwind.test;

public class SuperMember extends Member {
@Override
public void buyBook() {
// TODO Auto-generated method stub
System.out.println("超级会员买书打6折");
}
}

如果子类也是抽象类,则可以不用重写父类的抽象方法。(孙子类还可以去实现类)

接口

什么是接口?

接口是由抽象类衍生出来的一个概念,并由此产生了一种编程方式:面向接口编程。

优点:可用性,维护性,扩展性

面向接口编程就是将程序中的业务模块进行分离,以接口的形式去对接不同的业务模块。

面向接口编程的优点:当用户需求变更时,只需要切换不同的实现类,而不需要修改串联模块的接口,减少对系统的影响。

1、能够最大限度实现解耦合,降低程序的耦合性。

2、使程序易于扩展。

3、有利于程序的后期维护。

如何使用接口?

接口在 Java 中时独立存在的一种结构,和类相似,我们需要创建一个接口文件,Java 中用 class 关键字来标识类,用 interface 来标识接口,基本语法:

1
2
3
public interface 接口名{
public 返回值类型 方法名(参数列表);
}

接口其实就是一个抽象类,极度抽象的抽象类。

抽象类:一个类中一旦存在没有具体实现的抽象方法时,那么该类就必须定义为抽象类,同时抽象类允许存在非抽象方法。

但是接口完全不同,接口中不能存在非抽象方法,接口中必须全部是抽象方法,正因如此,所以修饰抽象方法的关键字 abstract 可以省略

接口中允许定义成员变量,但是有如下要求:

  1. 不能定义 private 和 protected 修饰的成员变量,只能定义 public 和 默认访问权限修饰符 修饰的成员变量。

  2. 接口中的成员变量在定义时就必须完成初始化(赋值)。

  3. 接口中的成员变量都是静态常量,即可以直接通过接口访问,同时值不能被修改。

1
2
3
4
5
6
7
package com.southwind.test;

public interface MyInterface {
public int ID = 0;
String NAME = "张三";
public void test();
}

使用接口时,不能直接实例化接口对象,而必须实例化其实现类对象,实现类本身就是一个普通的 Java 类,创建实现类的代码如下所示。

1
2
3
4
5
6
7
8
9
package com.southwind.test;

public class MyInterfaceImpl implements MyInterface {
@Override
public void test() {
// TODO Auto-generated method stub

}
}

通过 implements 关键字来指定实现类具体要实现的接口,在实现类的内部需要对接口的所有抽象方法进行实现,同时要求访问权限修饰符、返回值类型、方法名和参数列表必须完全一致。

接口和继承,Java 只支持单继承,但是接口可以多实现(一个实现类可以同时实现多个接口)

1
2
3
4
5
6
7
package com.southwind.test;

public interface MyInterface {
public int ID = 0;
String NAME = "张三";
public void fly();
}
1
2
3
4
5
package com.southwind.test;

public interface MyInterface2 {
public void run();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.southwind.test;

public class MyInterfaceImpl implements MyInterface, MyInterface2 {

@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("实现了跑步的方法");
}

@Override
public void fly() {
// TODO Auto-generated method stub
System.out.println("实现了飞行的方法");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
package com.southwind.test;

import java.util.ArrayList;
import java.util.List;

public class Test {
public static void main(String[] args) {
MyInterfaceImpl myInterfaceImpl = new MyInterfaceImpl();
myInterfaceImpl.fly();
myInterfaceImpl.run();
}
}

面向接口编程的实际应用

面向接口编程是一种常用的编程方式,可以有效地提高代码的复用性,增强程序的扩展性和维护性。

  • 场景

某工厂生产产品 A,产品 A 主要是由设备 A 来完成生产,用程序模拟生产过程。

产品 B 是由设备 B 来生产的。

当需求发生改变时,就要频繁修改类的内部结构的方式是需要避免的,因为这种结构的程序扩展性很差,如何改进?使用面向接口编程即可。

1、创建接口 Equiment。

1
2
3
4
5
package com.southwind.test;

public interface Equiment {
public void work();
}

2、创建 Equiment 的实现类。

1
2
3
4
5
6
7
package com.southwind.test;

public class EquipmentA implements Equiment {
public void work() {
System.out.println("设备A运行,生产产品A");
}
}
1
2
3
4
5
6
7
package com.southwind.test;

public class EquipmentB implements Equiment {
public void work() {
System.out.println("设备B运行,生产产品B");
}
}
1
2
3
4
5
6
7
8
9
package com.southwind.test;

public class EquipmentC implements Equiment {
public void work() {
// TODO Auto-generated method stub
System.out.println("设备C运行,生产产品C");
}

}

3、创建 Factory 类。

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

public class Factory {
private Equiment equipment;

public Equiment getEquipment() {
return equipment;
}

public void setEquipment(Equiment equipment) {
this.equipment = equipment;
}

public void work() {
System.out.println("开始生产...");
this.equipment.work();
}
}

4、Test

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

public class Test {
public static void main(String[] args) {
//初始化工厂
Factory factory = new Factory();
// EquipmentB equipmentB = new EquipmentB();
// factory.setEquipment(equipmentB);
// EquipmentA equipmentA = new EquipmentA();
// factory.setEquipment(equipmentA);
EquipmentC equipmentC = new EquipmentC();
factory.setEquipment(equipmentC);
//开始工作
factory.work();
}
}

常用类

Object

Object 是 Java 官方提供的类,存放在 java.lang 包中,该类是所有类的直接父类或者间接父类,无论是 Java 提供的类还是开发者自定义的类,都是 Object 的直接子类或间接子类,Java 中的任何一个类都会继承 Object 中的 public 和 protected 方法。

1
2
3
4
5
6
7
8
9
10
hashCode();
getClass();
equals(null);
clone();
toString();
notify();
notifyAll();
wait();
wait(1000L);
wait(1000L, 100);

Object 类中经常被子类重写的方法:

1、public String toString() 以字符串的形式返回对象的信息

2、public boolean equals(Object obj) 判断两个对象是否相等

3、public native int hashCode() 返回对象的散列码

  • toString

Object

1
2
3
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

重写之后

1
2
3
4
@Override
public String toString() {
return "People [id=" + id + ", name=" + name + ", score=" + score + "]";
}
  • equals

Object

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

重写之后

1
2
3
4
5
6
7
8
9
@Override
public boolean equals(Object obj) {
// TODO Auto-generated method stub
People people = (People)obj;
if(this.id == people.id && this.name.equals(people.name) && this.score.equals(people.score)) {
return true;
}
return false;
}
  • hashCode

Object

1
public native int hashCode();

重写之后

1
2
3
4
5
@Override
public int hashCode() {
// TODO Auto-generated method stub
return (int) (id*name.hashCode()*score);
}

包装类

  • 什么是包装类?

包装类是 Java 提供的一组类,专门用来创建 8 种基本数据类型对应的对象,一共有 8 个包装类,存放在 java.lang 包中,基本数据类型对应的包装类。

基本数据类型包装类
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
booleanBoolean

包装类的体系结构

装箱和拆箱

装箱和拆箱是包装类的特有名词,装箱是指将基本数据类型转为对应的包装类对象,拆箱就是将包装类对象转为对应的基本数据类型。

装箱与拆箱

装箱是指将基本数据类型转换为包装类对象。

拆箱是指将包装类对象转换为基本数据类型。

构造函数

public Type(type value)【即原类型

每个包装类都提供了一个有参构造函数:public Type(type 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
byte b = 1;
Byte byt = new Byte(b);
short s = 2;
Short shor = new Short(s);
int i = 3;
Integer integer = new Integer(i);
long l = 4;
Long lon = new Long(l);
float f = 5.5f;
Float flo = new Float(f);
double d = 6.6;
Double dou = new Double(d);
char cha = 'J';
Character charac = new Character(cha);
boolean bo = true;
Boolean bool = new Boolean(bo);
System.out.println(byt);
System.out.println(shor);
System.out.println(integer);
System.out.println(lon);
System.out.println(flo);
System.out.println(dou);
System.out.println(charac);
System.out.println(bool);

2、public Type(String value)/public Type(char value)【即字符/字符串类型

每个包装类还有一个重载构造函数:

Character 类的重载构造函数:public Type(char value),其他包装类的重载构造函数:public Type(String value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Byte byt = new Byte("1");
Short shor = new Short("2");
Integer integer = new Integer("3");
Long lon = new Long("4");
Float flo = new Float("5.5f");
Double dou = new Double("6.6");
Character charac = new Character('J');
Boolean bool = new Boolean("abc");
System.out.println(byt);
System.out.println(shor);
System.out.println(integer);
System.out.println(lon);
System.out.println(flo);
System.out.println(dou);
System.out.println(charac);

需要注意的是,Boolean 类的构造函数中,当参数为 "true" 时,Boolean 值为 true,当参数不为 "true",Boolean 值为 false。

装箱

1、public Type(type value)

2、public Type(String value)/public Type(char value)

3、valueOf(type value) 静态方法,参数是基本数据类型的数据

每一个包装类都有一个 valueOf(type value) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
byte b = 1;
Byte byt = Byte.valueOf(b);
short s = 2;
Short shot = Short.valueOf(s);
int i = 3;
Integer integer = Integer.valueOf(i);
long l = 4L;
Long lon = Long.valueOf(l);
float f = 5.5f;
Float floa = Float.valueOf(f);
double d = 6.6;
Double doub = Double.valueOf(d);
boolean boo = true;
Boolean bool = Boolean.valueOf(boo);
char ch = 'J';
Character cha = Character.valueOf(ch);

其中:

1
2
3
4
5
6
7
8
9
10
// valueOf(String value)/valueOf(char value) 专门为 Character 转换使用的
// 其他的 7 个包装类都可以使用 valueOf(String value)。
Byte byt = Byte.valueOf("1");
Short sho = Short.valueOf("2");
Integer integer = Integer.valueOf("3");
Long lon = Long.valueOf("4");
Float flo = Float.valueOf("5.5f");
Double dou = Double.valueOf("6.6");
Boolean boo = Boolean.valueOf("true");
Character cha = Character.valueOf('J');

需要注意的是 Boolean.valueOf(String value) 方法中,当 value 为 "true" 时,Boolean 的值为 true,否则,Boolean 的值为 false。

拆箱

1、*Value()

每个包装类都有一个 *Value() 方法,通过该方法可以将包装类转为基本数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Byte byt = Byte.valueOf("1");
Short sho = Short.valueOf("2");
Integer integer = Integer.valueOf("3");
Long lon = Long.valueOf("4");
Float flo = Float.valueOf("5.5f");
Double dou = Double.valueOf("6.6");
Boolean boo = Boolean.valueOf("true");
Character cha = Character.valueOf('J');

byte b = byt.byteValue();
short sh = sho.shortValue();
int i = integer.intValue();
long l = lon.longValue();
float f = flo.floatValue();
double d = dou.doubleValue();
boolean bo = boo.booleanValue();
char c = cha.charValue();

2、parse*(String value)

除了 Character 类以外的每一个包装类都有一个静态方法可以将字符串类型转为基本数据类型。

1
2
3
4
5
6
7
byte b = Byte.parseByte("1");
short s = Short.parseShort("2");
int i = Integer.parseInt("3");
long l = Long.parseLong("4");
float f = Float.parseFloat("5.5");
double d = Double.parseDouble("6.6");
boolean bo = Boolean.parseBoolean("true");

3、toString(type value)

每个包装类都有该方法,作用是将基本数据类型转为 String 类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
byte b = 1;
String bstr = Byte.toString(b);
short s = 2;
String sstr = Short.toString(s);
String i = Integer.toString(3);
long l = 4L;
String lstr = Long.toString(l);
float f = 5.5f;
String fstr = Float.toString(f);
double d = 6.6;
String dstr = Double.toString(d);
boolean bo = true;
String bostr = Boolean.toString(bo);
String chstr = Character.toString('J');

异常

基本概念

  • 什么是异常?

Java 中的错误大致可以分为两类:

一类是编译时错误,一般是指语法错误。

另一类是运行时错误。

Java 中有一组专门用来描述各种不同的运行时异常,叫做异常类,Java 结合异常类提供了处理错误的机制。

具体步骤是当程序出现错误时,会创建一个包含错误信息的异常类的实例化对象,并自动将该对象提交给系统,由系统转交给能够处理异常的代码进行处理。

异常可以分为两类:

【Error 和 Exception】:

Error 是指系统错误,JVM 生成,我们编写的程序无法处理。

Exception 指程序运行期间出现的错误,我们编写的程序可以对其进行处理。

  • 异常的使用

异常的使用需要用到两个关键字 try 和 catch,并且这两个关键字需要结合起来使用,用 try 来监听可能会抛出异常的代码,一旦捕获到异常,生成异常对象并交给 catch 来处理,基本语法如下所示。

1
2
3
4
5
try{
//可能抛出异常的代码
} catch(Exception e) {
//处理异常
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.southwind.exception;

public class Test {
public static void main(String[] args) {
try {
int num = 10/10;
}catch (Exception e) {
// TODO: handle exception
if(e.getMessage().equals("/ by zero")) {
System.err.println("分母不能为0");
}
}
}
}

除了 try 和 catch,还可以使用 finally 关键字来处理异常,finally 的作用?

无论程序是否抛出异常,finally 代码块中的代码一定都会执行,finally 一般跟在 catch 代码块的后面,基本语法如下所示。

1
2
3
4
5
6
7
try{
//可能抛出异常的代码
}catch(Exception e){
//处理异常
}finally{
//必须执行的代码
}
  • 异常类

Java 将运行时出现的错误全部封装成类,并且不是一个类,而是一组类。同时这些类之间是有层级关系的,由树状结构一层层向下分级,处在最顶端的类是 Throwable,是所有异常类的根结点。

Throwable 有两个直接子类:Error 和 Exception。

Error 的子类包括 VirtualMachineError、AWTError、IOError。

VirtualMachineError 的子类有 StackOverflowError、OutOfMemoryError。

Exception 的子类包括 IOException 和 RuntimeException。

IOException 的子类包括 FileLockInterruptionException、FileNotFoundException、FilerException。

RuntimeException 的子类包括 ArithmeticException、ClassNotFoundException、IllegalArggumentException、ArrayIndexOutOfBoundsException、NullPointerException、NoSuchMethodException、NumberFormatException。

异常

try-catch-finally

1
2
3
4
5
6
7
try{
//可能会抛出异常的代码
}catch(Exception e){
//对异常进行处理
}finally{
//一定会执行的代码
}

异常类

  • Error:系统错误,程序无法处理。
  • Exception:程序运行时出现的错误,程序可以处理。

Error 和 Exception 都是 Throwable 的子类,Throwable、Error、Exception 都是存放在 java.lang 包中。

Error 常见的子类:VirtualMachineError、AWTError、IOError。

VirtualMachineError 的子类:StackOverflowError、OutOfMemoryError,用来描述内存溢出等系统问题。

Exception 常见的子类:IOException 和 RuntimeException

IOException 存放在 java.io 包中,RuntimeException 存放在 java.lang 包中。

IOException 常见的子类:FileLockInterruptionException、FileNotFoundException、FilerException,这些异常通常都是处理通过 IO 流进行文件传输的时候发生的错误。

RuntimeException 常见的子类:

  • ArithmeticException:表示数学运算异常。
  • ClassNotFoundException:表类未定义异常。
  • IllelArgumentException:表示参数格式错误。
  • ArrayIndexOutOfBounds:表示数组下标越界。
  • NullPointException:表示空指针异常。
  • NoSuchMethodException:表示方法未定义异常。
  • NumberFormatException:表示将其他数据类型转为数值类型发生的类型不匹配异常。

throw 和 throws

throw 和 throws 是 Java 在处理异常时使用的两个关键字,都可以用来抛出异常,但是使用的方式和表示的含义完全不同。

Java 中抛出异常有 3 种方式:

  • try-catch
  • 使用 throw 是开发者主动抛出异常,即读到 throw 代码就一定抛出异常,基本语法:throw new Exception(),是一种基于代码的逻辑而主动抛出异常的方式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
int[] array = {1,2,3};
test(array,2);
}

public static void test(int[] array,int index) {
if(index >= 3 || index < 0) {
try {
throw new Exception();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}else {
System.out.println(array[index]);
}
}
}
  • try-catch 和 throw 都是作用于具体的逻辑代码,throws 是作用于方法的,用来描述方法可能会抛出的异常。

如果方法 throws 的是 RuntimeException 异常或者其子类,外部调用时可以不处理,JVM 会处理。

如果方法 throws 的是 Exception 异常或者其子类,外部调用时必须处理,否则报错。

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) throws Exception {
test("123");
}

public static void test(String str) throws Exception {
int num = Integer.parseInt(str);
}

}

异常捕获

  • 自动捕获 try-cath

  • throw 主动抛出异常

  • throws 修饰可能抛出异常的方法

自定义异常

除了使用 Java 提供的异常外,也可以根据需求来自定义异常。

1
2
3
4
5
6
7
package com.southwind.exception;

public class MyNumberException extends RuntimeException {
public MyNumberException(String error) {
super(error);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.southwind.exception;

public class Test {
public static void main(String[] args){
Test test = new Test();
System.out.println(test.add("a"));
}

public int add(Object object){
if(object instanceof Integer) {
int num = (int)object;
return ++num;
}else {
String error = "传入的参数不是整数类型";
MyNumberException myNumberException = new MyNumberException(error);
throw myNumberException;
}
}
}

综合练习

封装、继承、多态、抽象、接口、异常完成一个汽车查询系统。

需求描述:共有 3 种类型的汽车:小轿车、大巴车、卡车,其中小轿车的座位数是 4 座,大巴车座位数是 53 座,卡车座位数是 2 座,要求使用封装、继承、抽象来完成车辆的定义。

可以对车辆信息进行修改,卡车可以运货但是载重量不能超过 12 吨,使用自定义异常来处理错误,小轿车和大巴车没有此功能,要求使用接口来实现。

Car

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

public abstract class Car {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public Car(String name, String color) {
super();
this.name = name;
this.color = color;
}
public abstract String seatNum();
}

Sedan

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

public class Sedan extends Car {

public Sedan(String name, String color) {
super(name, color);
}

@Override
public String seatNum() {
// TODO Auto-generated method stub
return "4座";
}

}

Bus

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

public class Bus extends Car {

public Bus(String name, String color) {
super(name, color);
// TODO Auto-generated constructor stub
}

@Override
public String seatNum() {
// TODO Auto-generated method stub
return "53座";
}

}

Truck

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

public class Truck extends Car implements Container {

private int weight;

public Truck(String name, String color,int weight) {
super(name, color);
this.weight = weight;
// TODO Auto-generated constructor stub
}

@Override
public String seatNum() {
// TODO Auto-generated method stub
return "2座";
}

@Override
public int getweight() {
// TODO Auto-generated method stub
return this.weight;
}

}

Container

1
2
3
4
5
package com.southwind.test;

public interface Container {
public int getweight();
}

CarException

1
2
3
4
5
6
7
package com.southwind.test;

public class CarException extends Exception {
public CarException(String error) {
super(error);
}
}

Test

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

import java.util.Scanner;

public class Test {
private static Scanner scanner;
private static Sedan sedan;
private static Bus bus;
private static Truck truck;
private static Car[] cars;
static {
scanner = new Scanner(System.in);
sedan = new Sedan("小轿车","黑色");
bus = new Bus("大巴车","绿色");
truck = new Truck("卡车","蓝色",2);
cars = new Car[3];
cars[0] = sedan;
cars[1] = bus;
cars[2] = truck;
}

public void showCars() {
System.out.println("欢迎使用本汽车管理系统");
System.out.println("车辆名称\t\t车辆颜色\t\t座位数\t\t载重量");
for(Car car:cars) {
if(car instanceof Truck) {
Truck truck = (Truck)car;
System.out.println(car.getName()+"\t\t"+car.getColor()+"\t\t"+car.seatNum()+"\t\t"+truck.getweight());
}else {
System.out.println(car.getName()+"\t\t"+car.getColor()+"\t\t"+car.seatNum()+"\t\t不能拉货");
}
}
System.out.println("1.小轿车\t2.大巴车\t3.卡车");
System.out.print("请选择要修改的车辆:");
int num = scanner.nextInt();
switch(num) {
case 1:
update("sedan");
break;
case 2:
update("bus");
break;
case 3:
update("truck");
break;
default:
System.out.println("车辆不存在!");
break;
}
}

public void update(String type) {
String name = null;
String color = null;
if(type.equals("sedan")) {
System.out.print("输入车辆名称");
name = scanner.next();
System.out.print("输入车辆颜色");
color = scanner.next();
Sedan sedan = new Sedan(name,color);
cars[0] = sedan;
}
if(type.equals("bus")) {
System.out.print("输入车辆名称");
name = scanner.next();
System.out.print("输入车辆颜色");
color = scanner.next();
Bus bus = new Bus(name,color);
cars[1] = bus;
}
if(type.equals("truck")) {
System.out.print("输入车辆名称");
name = scanner.next();
System.out.print("输入车辆颜色");
color = scanner.next();
System.out.print("输入载重量");
int weight = scanner.nextInt();
if(weight > 12) {
CarException carException = new CarException("卡车的载重量不能超过12吨");
try {
throw carException;
} catch (CarException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return;
}
}
Truck truck = new Truck(name,color,weight);
cars[2] = truck;
}
showCars();
}

public static void main(String[] args) {
Test test = new Test();
test.showCars();
}
}

讲解了面向对象的高级部分,包括 Object 类、包装类、接口和异常。其中 Object 类是所有 Java 类的父类,定义了 Java 体系的基础资料,通过继承传递给 Java 的每一个类,通过方法重写和多态让整个 Java 体系具有很强的灵活性。

包装类是 Java 为基本数据类型提供封装的一组类,通过包装类我们可以将基本数据类型转为对象,这一点在面向对象编程中很重要。

接口是抽象类的扩展,是 Java 中实现多态的重要方式,可以降低程序的耦合性,让程序变得更加灵活多变。接口就相当于零件,我们可以自由地将这些零件进行组装、整合。

异常是 Java 中处理错误的一种机制,同样是基于面向对象的思想,将错误抽象成对象然后进行处理,这里需要关注的是对异常相关的几个关键字的使用,try、catch、finally、throw、throws。

多线程

多线程

多线程是提升程序性能非常重要的一种方式,必须掌握的技术。

使用多线程可以让程序充分利用 CPU 资源。

优点:

  • 系统资源得到更合理的利用。
  • 程序设计更加简洁。
  • 程序响应更快,运行效率更高。

缺点:

  • 需要更多的内存空间来支持多线程。
  • 多线程并发访问的情况可能会影响数据的准确性。
  • 数据被多线程共享,可能会出现死锁的情况。

进程和线程

什么是进程:进程就是计算机正在运行的一个独立的应用程序。

进程是一个动态的概念,当我们启动某个应用的时候,进程就产生了,当我们关闭该应用的时候,进程就结束了,进程的生命周期就是我们在使用该软件的整个过程。

什么是线程?

线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。

应用程序是静态的,进程和线程是动态的,有创建有销毁,存在是暂时的,不是永久的。

进程和线程的区别

进程在运行时拥有独立的内存空间,即每个进程所占用的内存空间都是独立的,互不干扰。

线程是共享内存空间的,但是每个线程的执行都是相互独立的,单独的线程是无法执行的,由进程来控制多个线程的执行。

多线程

多线程是指在一个进程中,多个线程同时执行,这里说的同时执行并不是真正意义的同时执行。

系统会为每个线程分配 CPU 资源,在某个具体的时间段内 CPU 资源会被一个线程占用,在不同的时间段内由不同的线程来占用 CPU 资源,所以多个线程还是在交替执行,只不过因为 CPU 运行速度太快,我们感觉是在同时执行。

整个程序如果是一条回路,说明程序只有一个线程。

程序有两条回路,同时向下执行,这种情况就是多线程,两个线程同时在执行。

Java 中线程的使用

Java 中使用线程有两种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口

Java 写程序三部分组成:

1、JDK 系统类库

JRE:Java Runtime Enviroment(Java 运行环境),仅供运行程序的。

JDK:Java Development Kit(Java 开发工具包),如果需要进行程序开发,必须安装 JDK。

String、Scanner、包装类。。。

java.lang.Thread

javax.servlet.Servlet

2、第三方类库

非 Java 官方的组织提供的一些成熟好用的工具,C3P0 数据库连接池、Spring 框架、DBUtils、Dom4J...

github:全球最大的同性交友网站

3、开发者自定义的代码

根据具体的业务需求编写的业务代码。

Java 中线程的使用

  • 继承 Thread 类

1、创建自定义类并继承 Thread 类。

2、重写 Thread 类中的 run 方法,并编写该线程的业务逻辑代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.southwind.test;

public class MyThread extends Thread {

@Override
public void run() {
// TODO Auto-generated method stub
//定义业务逻辑
for(int i = 0;i<10;i++) {
System.out.println("-------------MyThread");
}
}

}

3、使用。

1
2
3
4
5
6
7
8
9
10
11
package com.southwind.test;

public class Test {
public static void main(String[] args) {
//开启两个子线程
MyThread thread1 = new MyThread();
MyThread2 thread2 = new MyThread2();
thread1.start();
thread2.start();
}
}

注意:不能通过 run 方法来调用线程的任务,因为 run 方法调用相当于普通对象的执行,并不会去抢占 CPU 资源。

只有通过 start 方法才能开启线程,进而去抢占 CPU 资源,当某个线程抢占到 CPU 资源后,会自动调用 run 方法。

  • 实现 Runnable 接口

1、创建自定义类并实现 Runnable 接口。

2、实现 run 方法,编写该线程的业务逻辑代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.southwind.test;

public class MyRunnable implements Runnable {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<1000;i++) {
System.out.println("========MyRunnable=======");
}
}

}

3、使用。

1
2
3
4
5
6
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
MyRunnable2 runnable2 = new MyRunnable2();
Thread thread2 = new Thread(runnable2);
thread2.start();

线程和任务:

线程是去抢占 CPU 资源的,任务是具体执行业务逻辑的,线程内部会包含一个任务,线程启动(start),当抢占到资源之后,任务就开始执行(run)。

两种方式的区别:

1、MyThread,继承 Thread 类的方式,直接在类中重写 run 方法,使用的时候,直接实例化 MyThread,start 即可,因为 Thread 内部存在 Runnable。

2、MyRunnbale,实现 Runnable 接口的方法,在实现类中重写 run 方法,使用的时候,需要先创建 Thread 对象,并将 MyRunnable 注入到 Thread 中,Thread.start。

实际开发中推荐使用第二种方式。

在线画图软件:

https://www.processon.com/diagrams

线程的状态

线程共有 5 种状态,在特定的情况下,线程可以在不同的状态之间切换,5 种状态如下所示。

  • 创建状态:实例化一个新的线程对象,还未启动。
  • 就绪状态:创建好的线程对象调用 start 方法完成启动,进入线程池等待抢占 CPU 资源。
  • 运行状态:线程对象获取了 CPU 资源,在一定的时间内执行任务。
  • 阻塞状态:正在运行的线程暂停执行任务,释放所占用的 CPU 资源,并在解除阻塞状态之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取 CPU 资源。
  • 终止状态:线程运行完毕或因为异常导致该线程终止运行。

线程状态之间的转换图。

Java 多线程的实现

  • 继承 Thread
  • 实现 Runnable

线程调度

  • 线程休眠

让当前线程暂停执行,从运行状态进入阻塞状态,将 CPU 资源让给其他线程的调度方式,通过 sleep() 来实现。

sleep(long millis),调用时需要传入休眠时间,单位为豪秒。

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

public class MyThread extends Thread{
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++) {
if(i == 5) {
try {
sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(i+"---------MyThread");
}
}
}

也可以在类的外部调用 sleep 方法。

1
2
3
4
5
6
7
8
MyThread2 thread = new MyThread2();
try {
thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
thread.start();

在外部调用需要注意,休眠一定要放在启动之前。

如何让主线程休眠?直接通过静态方式调用 sleep 方法。

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

public class Test2 {
public static void main(String[] args) {
for(int i=0;i<10;i++) {
if(i == 5) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(i+"+++++Test2+++++");
}
}
}
1
public static native void sleep(long millis) throws InterruptedException;

sleep 是静态本地方法,可以通过类调用,也可以通过对象调用,方法定义抛出 InterruptedException,InterruptedException 继承 Exception,外部调用时必须手动处理异常。

  • 线程合并

合并是指将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个自线程顺序执行。

通过调用 join 方法来实现合并,具体如何合并?

线程甲和线程乙,线程甲执行到某个时间点的时候调用线程乙的 join 方法,则表示从当前时间点开始 CPU 资源被线程乙独占,线程甲进入阻塞状态,直到线程乙执行完毕,线程甲进入就绪状态,等待获取 CPU 资源进入运行状态。

join 方法重载,join() 表示乙线程执行完毕之后才能执行其他线程,join(long millis) 表示乙线程执行 millis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺 CPU 资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.southwind.test;

public class JoinRunnable implements Runnable {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<200;i++) {
System.out.println(i+"------JoinRunnable");
}
}

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

public class JoinTest {
public static void main(String[] args) {
/**
* 两个线程,主线程、join线程
* 主线程的逻辑:当i==10,join线程合并到主线程中
*/
JoinRunnable joinRunnable = new JoinRunnable();
Thread thread = new Thread(joinRunnable);
thread.start();
for(int i=0;i<100;i++) {
if(i == 10) {
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(i+"main+++++++++");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.southwind.test;

public class JoinRunnable2 implements Runnable {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<20;i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(i+"--------JoinRunnable");
}
}

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

public class Test2 {
public static void main(String[] args) {
for(int i=0;i<10;i++) {
if(i == 5) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(i+"+++++Test2+++++");
}
}
}

线程礼让

线程礼让是指在某个特定的时间点,让线程暂停抢占 CPU 资源的行为,运行状态/就绪状态---》阻塞状态,将 CPU 资源让给其他线程来使用。

假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了 CPU 资源,执行业务逻辑,但不代表线程甲一直暂停执行。

线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺 CPU 资源。

通过 yield 方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.southwind.yield;

public class YieldThread1 extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 10;i++) {
if(i == 5) {
yield();
}
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.southwind.yield;

public class YieldThread2 extends Thread {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"======"+i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.southwind.yield;

public class Test {
public static void main(String[] args) {
YieldThread1 thread = new YieldThread1();
thread.setName("线程1");
YieldThread2 thread2 = new YieldThread2();
thread2.setName("线程2");
thread.start();
thread2.start();
}
}

线程中断

有很多种情况会造成线程停止运行:

线程执行完毕自动停止

线程执行过程中遇到错误抛出异常并停止

线程执行过程中根据需求手动停止

Java 中实现线程中断有如下几个常用方法:

  • public void stop()
  • public void interrupt()
  • public boolean isInterrupted()

stop 方法在新版本的 JDK 已经不推荐使用,重点关注后两个方法。

interrupt 是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象。

每个线程对象都是通过一个标志位来判断当前是否为中断状态。

isInterrupted 就是用来获取当前线程对象的标志位:true 表示清除了标志位,当前线程已经中断;false 表示没有清除标志位,当前对象没有中断。

当一个线程对象处于不同的状态时,中断机制也是不同的。

创建状态:实例化线程对象,不启动。

1
2
3
4
5
6
7
8
9
10
package com.southwind.interrupted;

public class Test {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}

NEW 表示当前线程对象为创建状态,false 表示当前线程并未中断,因为当前线程没有启动,不存在中断,不需要清除标志位。

匿名内部类

1
2
3
4
5
6
7
8
9
10
11
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 10;i++) {
System.out.println(i+"---main");
}
}
});
thread.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 com.southwind.interrupted;

public class Test2 {
public static void main(String[] args) {
// MyRunnable runnable = new MyRunnable();
// Thread thread = new Thread(runnable);
// thread.start();

Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
for(int i = 0; i < 10;i++) {
System.out.println(i+"---main");
}
}
});
thread.start();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}

线程同步

Java 中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作。

多个线程同时操作同一个共享数据时,可能会导致数据不准确的问题。

使用线程同步可以解决上述问题。

可以通过 synchronized 关键字修饰方法实现线程同步,每个 Java 对象都有一个内置锁,内置锁会保护使用 synchronized 关键字修饰的方法,要调用该方法就必须先获得锁,否则就处于阻塞状态。

非线程同步

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

public class Account implements Runnable {

private static int num;

@Override
public void run() {
// TODO Auto-generated method stub
//1.num++操作
num++;
//2.休眠1毫秒
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//3.打印输出
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访问");
}

}

线程同步

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

public class Account implements Runnable {

private static int num;

@Override
public synchronized void run() {
// TODO Auto-generated method stub
//1.num++操作
num++;
//2.休眠1毫秒
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//3.打印输出
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访问");
}

}
1
2
3
4
5
6
7
8
9
10
11
package com.southwind.test;

public class Test {
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new Thread(account,"张三");
Thread t2 = new Thread(account,"李四");
t1.start();
t2.start();
}
}

synchronized 关键字可以修饰实例方法,也可以修饰静态方法,两者在使用的时候是有区别的。

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

public class SynchronizedTest {

public static void main(String[] args) {
for(int i = 0; i < 5;i++) {
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
SynchronizedTest.test();
}
});
thread.start();
}
}

/**
* 先输出start...
* 间隔1s
* 再输出end...
* 输出start...
* ...
*/
public synchronized static void test() {
//1.输出start
System.out.println("start......");
//2.休眠
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//3.输出end
System.out.println("end......");
}

}

synchronized 修饰非静态方法

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

public class SynchronizedTest2 {
public static void main(String[] args) {
for(int i=0;i<5;i++) {
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
synchronizedTest2.test();
}
});
thread.start();
}
}

public synchronized void test() {
System.out.println("start......");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("end......");
}
}

给实例方法(非静态方法)添加 synchronized 关键字并不能实现线程同步。

线程同步的本质是锁定多个线程所共享的资源,synchronized 还可以修饰代码块,会为代码块加上内置锁,从而实现同步。

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

public class SynchronizedTest3 {

public static void main(String[] args) {
for(int i=0;i<5;i++) {
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
SynchronizedTest3.test();
}
});
thread.start();
}
}

public static void test() {

synchronized (SynchronizedTest3.class) {
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("end...");
}

}

}

如何判断线程同步或是不同步?

找到关键点:锁定的资源在内存中是一份还是多份?一份大家需要排队,线程同步,多份(一人一份),线程不同步。

无论是锁定方法还是锁定对象,锁定类,只需要分析这个方法、对象、类在内存中有几份即可。

对象一般都是多份

类一定是一份

方法就看是静态方法还是非静态方法,静态方法一定是一份,非静态方法一般是多份

线程安全的单例模式

单例模式是一种常见的软件设计模式,核心思想是一个类只有一个实例对象。

JVM:栈内存、堆内存

单线程模式下的单例模式

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

public class SingletonDemo {

private static SingletonDemo singletonDemo;

private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}

public static SingletonDemo getInstance() {
if(singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}

}

多线程模式下的单例模式

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

public class SingletonDemo {

private static SingletonDemo singletonDemo;

private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}

public synchronized static SingletonDemo getInstance() {
if(singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}

}

双重检测,synchronized 修饰代码块。

1、线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的。

2、如果 synchronized 锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的。

3、

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

public class SingletonDemo {

private volatile static SingletonDemo singletonDemo;

private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}

public static SingletonDemo getInstance() {
if(singletonDemo == null) {
synchronized (SingletonDemo.class) {
if(singletonDemo == null) {
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}

}

volatile 的作用时候可以使内存中的数据对象线程可见。

主内存对线程是不可见的,添加 volatile 关键字之后,主内存对线程可见。

线程同步

并发、并行

使用并发编程的目的?为了充分利用计算机的资源,提高性能,企业以盈利为目的。

并发:多个线程访问同一个共享资源,前提是计算机是单核 CPU,多个线程不是同时在访问,而是交替进行,只是因为 CPU 运行速度太快,看起来是同时在运行。

并行:多核 CPU,多个线程是真正的同时在运行,各自占用不同的 CPU,相互之间没有影响,也不会争夺资源。

Java 默认线程有两个,main(主线程),GC(垃圾回收机制)

synchronized 关键字实现线程同步,让在访问同一个资源的多个线程排队去完成业务,避免出现数据错乱的情况。

死锁 DeadLock

前提:一个线程完成业务需要同时访问两个资源。

死锁:多个线程同时在完成业务,出现争抢资源的情况。

资源类

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

public class DeadLockRunnable implements Runnable {
//编号
public int num;
//资源
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks();

/**
* num = 1 拿到 chopsticks1,等待 chopsticks2
* num = 2 拿到 chopsticks2,等待 chopsticks1
*/
@Override
public void run() {
// TODO Auto-generated method stub
if(num == 1) {
System.out.println(Thread.currentThread().getName()+"拿到了chopsticks1,等待获取chopsticks2");
synchronized (chopsticks1) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (chopsticks2) {
System.out.println(Thread.currentThread().getName()+"用餐完毕!");
}
}
}
if(num == 2) {
System.out.println(Thread.currentThread().getName()+"拿到了chopsticks2,等待获取chopsticks1");
synchronized (chopsticks2) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (chopsticks1) {
System.out.println(Thread.currentThread().getName()+"用餐完毕!");
}
}
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
package com.southwind.demo1;

public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1,"张三").start();
new Thread(deadLockRunnable2,"李四").start();
}
}

如何破解死锁

不要让多线程并发访问

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

public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1,"张三").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
new Thread(deadLockRunnable2,"李四").start();
}
}

使用 lambda 表达式简化代码开发

1
2
3
4
5
6
7
8
9
10
11
12
package com.southwind.demo2;

public class Test3 {
public static void main(String[] args) {
//lambda表达式
new Thread(()->{
for(int i=0;i<100;i++) {
System.out.println("+++++++++++Runnable");
}
}) .start();
}
}
1
2
3
4
5
6
7
8
9
package com.southwind.demo2;

public class Test3 {
public static void main(String[] args) {
new Thread(()->{for(int i=0;i<100;i++) System.out.println("+++++++Runnable");}).start();
new Thread(()->{for(int i=0;i<100;i++) System.out.println("----Runnable");}).start();
new Thread(()->{for(int i=0;i<100;i++) System.out.println("++++=====++Runnable");}).start();
}
}

Lock

JUC

java.util.concurrent

Lock 是一个接口,用来实现线程同步的,功能与 synchronized 一样。

Lock 使用频率最高的实现类是 ReentrantLock(重入锁),可以重复上锁。

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

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

public class Test2 {
public static void main(String[] args) {
Account account = new Account();
new Thread(account,"A").start();
new Thread(account,"B").start();
}
}

class Account implements Runnable{

private static int num;
private Lock lock = new ReentrantLock();

@Override
public void run() {
// TODO Auto-generated method stub
lock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
lock.unlock();
}

}

实现资源和 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
package com.southwind.demo3;

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

public class Test2 {
public static void main(String[] args) {
Account account = new Account();
new Thread(()->{
account.count();
},"A").start();
new Thread(()->{
account.count();
},"B").start();
}
}

class Account {
private int num;
private Lock lock = new ReentrantLock();

public void count() {
lock.lock();
num++;
System.out.println(Thread.currentThread().getName()+"是第"+num+"位访客");
lock.unlock();
}

}

JUC

java.util.concurrent

Java 并发编程工具包,Java 官方提供的一套专门用来处理并发编程的工具集合(接口+类)

并发:单核 CPU,多个线程“同时”运行,实际是交替执行,只不过速度太快,看起来是同时执行。

两个厨师一口锅

并行:多核 CPU,真正的多个线程同时运行。

两个厨师两口锅

重入锁是 JUC 使用频率非常高的一个类 ReentrantLock

ReentrantLock 就是对 synchronized 的升级,目的也是为了实现线程同步。

  • ReentrantLock 是一个类,synchronized 是一个关键字。
  • ReentrantLock 是 JDK 实现,synchronized 是 JVM 实现。
  • synchronized 可以自动释放锁,ReentrantLock 需要手动释放。

ReentrantLock 是 Lock 接口的实现类。

公平锁和非公平锁的区别

公平锁:线程同步时,多个线程排队,依次执行

非公平锁:线程同步时,可以插队

线程的实现有两种方式

  • 继承 Thread
  • 实现 Runnable

实现 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
package com.southwind.demo;

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

public class Test {
public static void main(String[] args) {
Account account = new Account();
new Thread(()->{
account.count();
},"A") .start();
new Thread(()->{
account.count();
},"B") .start();
}
}

/**
* 将资源和 Runnable 进行解耦合
* @author southwind
*
*/
class Account{
private static int num;
public void count() {
num++;
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}
}
1
2
3
4
5
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 com.southwind.demo;

import java.util.concurrent.TimeUnit;

public class Test2 {
public static void main(String[] args) {
Account2 account = new Account2();
new Thread(account,"A").start();
new Thread(account,"B").start();
}
}

class Account2 implements Runnable{

private static int num;

@Override
public void run() {
// TODO Auto-generated method stub
num++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}

}

Tips

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Account{
private static Integer num = 0;
private static Integer id = 0;
public void count() {
synchronized (num) {
num++;
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
}
}
}

如果锁定 num 不能同步,锁定 id 可以同步,原因是什么?

synchronized 必须锁定唯一的元素才可以实现同步

num 的值每次都在变,所以 num 所指向的引用一直在变,所以不是唯一的元素,肯定无法实现同步。

id 的值永远不变,所以是唯一的元素,可以实现同步。

中断

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

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

public class Test3 {
public static void main(String[] args) {
Account3 account = new Account3();
new Thread(()->{
account.count();
},"A").start();
new Thread(()->{
account.count();
},"B").start();
}
}

class Account3{
private static int num;
private ReentrantLock reentrantLock = new ReentrantLock();

public void count() {
//上锁
reentrantLock.lock();
reentrantLock.lock();
num++;
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"是当前的第"+num+"位访客");
//解锁
reentrantLock.unlock();
reentrantLock.unlock();
}
}
  • Lock 上锁和解锁都需要开发者手动完成。
  • 可以重复上锁,上几把锁就需要解几把锁。

ReentrantLock 除了可以重入之外,还有一个可以中断的特点,可中断是指某个线程在等待获取锁的过程中可以主动过终止线程。

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

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

public class Test5 {
public static void main(String[] args) {
StopLock stopLock = new StopLock();
Thread t1 = new Thread(()->{
stopLock.service();
},"A");
Thread t2 =new Thread(()->{
stopLock.service();
},"B");
t1.start();
t2.start();
try {
TimeUnit.SECONDS.sleep(1);
t2.interrupt();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

class StopLock{

private ReentrantLock reentrantLock = new ReentrantLock();

public void service() {

try {
reentrantLock.lockInterruptibly();

System.out.println(Thread.currentThread().getName()+"get lock");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

} catch (InterruptedException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} finally {
reentrantLock.unlock();
}

}
}

重入锁

ReentrantLock 限时性:判断某个线程在一定的时间内能否获取锁,通过 tryLock 方法来实现

tryLock(long time,TimeUnit unit)

time 指时间数值

unit 时间单位

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

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

public class Test {
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
/**
* A 拿到锁,执行业务代码,休眠 5 秒钟
* B 尝试拿锁,需要在 3 秒钟之内拿到锁
*/
new Thread(()->{
timeLock.lock();
},"A").start();
new Thread(()->{
timeLock.lock();
},"B").start();
}
}

class TimeLock{

private ReentrantLock reentrantLock = new ReentrantLock();

public void lock(){
/**
* 尝试在3S内获取锁
*/
try {
if(reentrantLock.tryLock(3, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+" get lock");
TimeUnit.SECONDS.sleep(5);
}else{
System.out.println(Thread.currentThread().getName()+" not lock");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(reentrantLock.isHeldByCurrentThread()){
reentrantLock.unlock();
}
}
}
}

生产者消费者模式

在一个生产环境中,生产者和消费者在同一时间段内共享同一块缓冲区,生产者负责向缓冲区添加数据,消费者负责从缓冲区取出数据。

汉堡类

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

public class Hamburger {
private int id;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

@Override
public String toString() {
return "Hamburger{" +
"id=" + id +
'}';
}
}

容器类

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

public class Container {
public Hamburger[] array = new Hamburger[6];
public int index = 0;
/**
* 向容器中添加汉堡
*/
public synchronized void push(Hamburger hamburger){
while(index == array.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
array[index] = hamburger;
index++;
System.out.println("生产类一个汉堡"+hamburger);
}
/**
* 从容器中取出汉堡
*/
public synchronized Hamburger pop(){
while(index == 0){
//当前线程暂停
//让正在访问当前资源的线程暂停
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//唤醒之前暂停的线程
this.notify();
index--;
System.out.println("消费了一个汉堡"+array[index]);
return array[index];
}
}

生产者

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

import java.util.concurrent.TimeUnit;

/**
* 生产者
*/
public class Producer {
private Container container;
public Producer(Container container){
this.container = container;
}

public void product(){
for (int i = 0; i < 30; i++) {
Hamburger hamburger = new Hamburger(i);
this.container.push(hamburger);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

消费者

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

import java.util.concurrent.TimeUnit;

public class Consumer {
private Container container;

public Consumer(Container container) {
this.container = container;
}

public void consum(){
for (int i = 0; i < 30; i++) {
this.container.pop();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

测试类

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

public class Test {
public static void main(String[] args) {
Container container = new Container();
Producer producer = new Producer(container);
Consumer consumer = new Consumer(container);
new Thread(()->{
producer.product();
}).start();
new Thread(()->{
producer.product();
}).start();
new Thread(()->{
consumer.consum();
}).start();
new Thread(()->{
consumer.consum();
}).start();
new Thread(()->{
consumer.consum();
}).start();
}
}

多线程并发卖票

一场球赛的球票分 3 个窗口出售,共 15 张票,用多线程并发来模拟 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
package com.southwind.demo3;

import java.util.concurrent.TimeUnit;

public class Ticket {
//剩余球票
private int surpluCount = 15;
//已售出球票
private int outCount = 0;

public synchronized void sale(){
while(surpluCount > 0){
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(surpluCount == 0){
return;
}
surpluCount--;
outCount++;
if(surpluCount == 0){
System.out.println(Thread.currentThread().getName()+"售出第"+outCount+"张票,球票已售罄");
}else{
System.out.println(Thread.currentThread().getName()+"售出第"+outCount+"张票,剩余"+surpluCount+"张票");
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.southwind.demo3;

public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
ticket.sale();
},"A").start();

new Thread(()->{
ticket.sale();
},"B").start();

new Thread(()->{
ticket.sale();
},"C").start();
}
}

Java 并发编程

为什么很重要?

并发编程可以充分利用计算机的资源,把计算机的性能发挥到最大,可以最大程度节约公司的成本,提高效率。

什么是高并发

并发 VS 并行的区别

并发 concurrency:多线程“同时”操作同一个资源,并不是真正的同时操作,而是交替操作,单核 CPU 的情况下,资源按时间段分配给多个线程。张三李四王五使用一口锅炒菜,交替

并行 parallelism:是真正的多个线程同时执行,多核 CPU,每个线程使用一个 CPU 资源来运行。张三李四王五使用三口锅炒菜,同时进行

并发编程描述的是一种使系统允许多个任务可以在重叠的时间段内执行的设计结构,不是指多个任务在同一时间段内执行,而是指系统具备处理多个任务在同一时间段内同时执行的能力。

高并发是指我们设计的程序,可以支持海量任务的执行在时间段上重叠的情况。

高并发的标准:

  • QPS:每秒响应的 HTTP 请求数量,QPS 不是并发数。
  • 吞吐量:单位时间内处理的请求数,由 QPS 和并发数来决定。
  • 平均响应时间:系统对一个请求作出响应的评价时间。

QPS = 并发数 / 平均响应时间

  • 并发用户数:同时承载正常使用系统的用户人数

互联网分布式架构设计,提高系统并发能力的方式:

  • 垂直扩展
  • 水平扩展

垂直扩展

提升单机处理能力

1、提升单机的硬件设备,增加 CPU 核数,升级网卡,硬盘扩容,升级内存。

2、提升单机的架构性能,使用 Cache 提高效率,使用异步请求来增加单服务吞吐量,NoSQL 提升数据库访问能力。

水平扩展

集群:一个厨师搞不定,多雇几个厨师一起炒菜,多个人干同一件事情。

分布式:给厨师雇两个助手,一个负责洗菜,一个负责切菜,厨师只负责炒菜,一件事情拆分成多个步骤,由不同的人去完成。

站点层扩展:Nginx 反向代理,一个 Tomcat 跑不动,那就 10 个 Tomcat 去跑。

服务层扩展:RPC 框架实现远程调用,Spring Boot/Spring Cloud,Dubbo,分布式架构,将业务逻辑拆分到不同的 RPC Client,各自完成对应的业务,如果某项业务并发量很大,增加新的 RPC Client,就能扩展服务层的性能,做到理论上的无限高并发。

数据层扩展:在数据量很大的情况下,将原来的一台数据库服务器,拆分成多台,以达到扩充系统性能的目的,主从复制,读写分离,分表分库。

JUC

JDK 提供的一个工具包,专门用来帮助开发者完成 Java 并发编程。

进程和线程

Java 默认的线程数 2 个

  • mian 主线程
  • GC 垃圾回收机制

Java 本身是无法开启线程的,Java 无法操作硬件,只能通过调用本地方法,C++ 编写的动态函数库。

Java 中实现多线程有几种方式?

1、继承 Thread 类

2、实现 Runnable 接口

3、实现 Callable 接口

Callable 和 Runnable 的区别在于 Runnable 的 run 方法没有返回值,Callable 的 call 方法有返回值。

1
2
3
4
5
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 com.southwind.demo1;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class Test {
public static void main(String[] args) {
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask(myCallable);
Thread thread = new Thread(futureTask);
thread.start();
try {
String value = futureTask.get();
System.out.println(value);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

class MyCallable implements Callable<String>{

@Override
public String call() throws Exception {
System.out.println("callable");
return "hello";
}
}

sleep 和 wait

sleep 是让当前线程休眠,wait 是让访问当前对象的线程休眠。

sleep 不会释放锁,wait 会释放锁。

synchronized 锁定的是什么

1、synchronized 修饰非静态方法,锁定方法的调用者

2、synchronized 修饰静态方法,锁定的是类

3、synchronized 静态方法和实例方法同时存在,静态方法锁定的是类,实例方法锁定的是对象

Lock

JUC 提供的一种锁机制,功能和 synchronized 类似,是对 synchronized 的升级,它是一个接口。

它的常用实现类是 ReentrantLock。

synchronized 是通过 JVM 实现锁机制,ReentrantLock 是通过 JDK 实现锁机制。

synchronized 是一个关键字,ReentrantLock 是一个类。

重入锁:可以给同一个资源添加多把锁。

synchronized 是线程执行完毕之后自动释放锁,ReentrantLock 需要手动解锁。

用 synchronized 实现卖票

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

import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
}
}

class Ticket{
private Integer saleNum = 0;
private Integer lastNum = 30;

public synchronized void sale(){
if(lastNum > 0){
saleNum++;
lastNum--;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+saleNum+"张票,剩余"+lastNum+"张票");
}
}

}

用 Lock 完成卖票

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

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

public class Test2 {
public static void main(String[] args) {
Ticket2 ticket = new Ticket2();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"B").start();
}
}

class Ticket2{
private Integer saleNum = 0;
private Integer lastNum = 30;
private Lock lock = new ReentrantLock();

public void sale(){

lock.lock();
lock.lock();
if(lastNum > 0){
saleNum++;
lastNum--;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"卖出了第"+saleNum+"张票,剩余"+lastNum+"张票");
}
lock.unlock();
lock.unlock();
}
}

synchronized 和 lock 的区别

1、synchronized 自动上锁,自动释放锁,Lock 手动上锁,手动释放锁。

2、synchronized 无法判断是否获取到了锁,Lock 可以判断是否拿到了锁。

3、synchronized 拿不到锁就会一直等待,Lock 不一定会一直等待。

4、synchronized 是 Java 关键字,Lock 是接口。

5、synchronized 是非公平锁,Lock 可以设置是否为公平锁。

公平锁:很公平,排队,当锁没有被占用时,当前线程需要判断队列中是否有其他等待线程。

非公平锁:不公平,插队,当锁没有被占用时,当前线程可以直接占用,而不需要判断当前队列中是否有等待线程。

实际开发中推荐使用 Lock 的方式。

ReentrantLock 具备限时性的特点,可以判断某个线程在一定的时间段内能否获取到锁,使用 tryLock 方法,返回值是 boolean 类型,true 表示可以获取到锁,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
package com.southwind.demo2;

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

public class Test {
public static void main(String[] args) {
TimeLock timeLock = new TimeLock();
new Thread(()->{
timeLock.getLock();
},"A").start();
new Thread(()->{
timeLock.getLock();
},"B").start();
}
}

class TimeLock{
private ReentrantLock lock = new ReentrantLock();

public void getLock(){
try {
if(lock.tryLock(3, TimeUnit.SECONDS)){
System.out.println(Thread.currentThread().getName()+"拿到了锁");
TimeUnit.SECONDS.sleep(5);
}else{
System.out.println(Thread.currentThread().getName()+"拿不到锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}

}

生产者消费者模式

synchronized

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

import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.decrement();
}
},"B").start();
}
}

class Data{
private Integer num = 0;

public synchronized void increment(){
while(num!=0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
this.notify();
System.out.println(Thread.currentThread().getName()+"生产了汉堡"+num);
}

public synchronized void decrement(){
while(num == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
this.notify();
System.out.println(Thread.currentThread().getName()+"消费了汉堡"+num);
}
}

必须使用 while 判断,不能用 if,因为 if 会存在线程虚假唤醒的问题,虚假唤醒就是一些 wait 方法会在除了 notify 的其他情况被唤醒,不是真正的唤醒,使用 while 完成多重检测,避免这一问题。

Lock

1
2
3
4
5
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 com.southwind.demo4;

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

public class Test {
public static void main(String[] args) {
Data data = new Data();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.increment();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 30; i++) {
data.decrement();
}
},"B").start();
}
}

class Data{
private Integer num = 0;

private Lock lock = new ReentrantLock();

private Condition condition = lock.newCondition();

public void increment(){
lock.lock();
while(num!=0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
condition.signal();
System.out.println(Thread.currentThread().getName()+"生产了汉堡"+num);
lock.unlock();
}

public synchronized void decrement(){
lock.lock();
while(num == 0){
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
condition.signal();
System.out.println(Thread.currentThread().getName()+"消费了汉堡"+num);
lock.unlock();
}
}

使用 Lock 锁,就不能通过 wait 和 notify 来暂停线程和唤醒线程,而应该使用 Condition 的 await 和 signal 来暂停和唤醒线程。

ConcurrentModificationException

并发访问异常

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

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//写
list.add("a");
//读
System.out.println(list);
},String.valueOf(i)).start();
}
}
}

如何解决?

1、Vector

2、Collections.synchronizedList

3、JUC:CopyOnWriteArrayList

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

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

public class Test2 {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread((()->{
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add("a");
System.out.println(list);
})).start();
}
}
}

CopyOnWrite 写时复制,当我们往一个容器添加元素的时候,不是直接给容器添加,而是先将当前容器复制一份,向新的容器中添加数据,添加完成之后,再将原容器的引用指向新的容器。

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
package com.southwind.demo5;

import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;

public class Test2 {
public static void main(String[] args) {
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread((()->{
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
set.add(String.valueOf(temp)+"a");
System.out.println(set);
})).start();
}
}
}

Map

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

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;

public class Test2 {
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 10; i++) {
final int temp = i;
new Thread((()->{
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
map.put(UUID.randomUUID().toString().substring(0,3),UUID.randomUUID().toString().substring(0,2));
System.out.println(map);
})).start();
}
}
}

JUC 工具类

CountDownLatch:减法计数器

可以用来倒计时,当两个线程同时执行时,如果要确保一个线程优先执行,可以使用计数器,当计数器清零的时候,再让另一个线程执行。

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

import java.util.concurrent.CountDownLatch;

public class Test {
public static void main(String[] args) {
//创建一个 CountDownLatch
CountDownLatch countDownLatch = new CountDownLatch(100);

new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("+++++++++++++++Thread");
countDownLatch.countDown();
}
}).start();

try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

for (int i = 0; i < 100; i++) {
System.out.println("main-----------------");
}
}
}

coutDown():计数器减一

await():计数器停止,唤醒其他线程

new CountDownLatch(100)、coutDown()、await() 必须配合起来使用,创建对象的时候赋的值是多少,coutDown() 就必须执行多少次,否则计数器是没有清零的,计数器就不会停止,其他线程也无法唤醒,所以必须保证计数器清零,coutDown() 的调用次数必须大于构造函数的参数值。

CyclicBarrier:加法计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(100,()->{
System.out.println("放行");
});

for (int i = 0; i < 100; i++) {
final int temp = i;
new Thread(()->{
System.out.println("-->"+temp);
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}

await():在其他线程中试图唤醒计数器线程,当其他线程的执行次数达到计数器的临界值时,则唤醒计数器线程,并且计数器是可以重复使用的,当计数器的线程执行完成一次之后,计数器自动清零,等待下一次执行。

new CyclicBarrier(30),for 执行 90 次,则计数器的任务会执行 3 次。

Semaphore:计数信号量

实际开发中主要使用它来完成限流操作,限制可以访问某些资源的线程数量。

Semaphore 只有 3 个操作:

  • 初始化
  • 获取许可
  • 释放
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SemaphoreTest {
public static void main(String[] args) {
//初始化
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 15; i++) {
new Thread(()->{
//获得许可
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"进店购物");
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName()+"出店");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
//释放
semaphore.release();
}

},String.valueOf(i)).start();
}
}
}

每个线程在执行的时候,首先需要去获取信号量,只有获取到资源才可以执行,执行完毕之后需要释放资源,留给下一个线程。

读写锁

接口 ReadWriteLock,实现类是 ReentrantReadWriteLock,可以多线程同时读,但是同一时间内只能有一个线程进行写入操作。

读写锁也是为了实现线程同步,只不过粒度更细,可以分别给读和写的操作设置不同的锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ReadWriteLockTest {
public static void main(String[] args) {
Cache cache = new Cache();
for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{

cache.write(temp,String.valueOf(temp));

}).start();
}

for (int i = 0; i < 5; i++) {
final int temp = i;
new Thread(()->{
cache.read(temp);
}).start();
}
}
}

class Cache{
private Map<Integer,String> map = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

/**
* 写操作
*/
public void write(Integer key,String value){
readWriteLock.writeLock().lock();
System.out.println(key+"开始写入");
map.put(key,value);
System.out.println(key+"写入完毕");
readWriteLock.writeLock().unlock();
}

/**
* 读操作
*/
public void read(Integer key){
readWriteLock.readLock().lock();
System.out.println(key+"开始读取");
map.get(key);
System.out.println(key+"读取完毕");
readWriteLock.readLock().unlock();
}
}

写入锁也叫独占锁,只能被一个线程占用,读取锁也叫共享锁,多个线程可以同时占用。

线程池

预先创建好一定数量的线程对象,存入缓冲池中,需要用的时候直接从缓冲池中取出,用完之后不要销毁,还回到缓冲池中,为了提高资源的利用率。

优势:

  • 提高线程的利用率
  • 提高响应速度
  • 便于统一管理线程对象
  • 可以控制最大的并发数

1、线程池初始化的时候创建一定数量的线程对象。

2、如果缓冲池中没有空闲的线程对象,则新来的任务进入等待队列。

3、如果缓冲池中没有空闲的线程对象,等待队列也已经填满,可以申请再创建一定数量的新线程对象,直到到达线程池的最大值,这时候如果还有新的任务进来,只能选择拒绝。

无论哪种线程池,都是工具类 Executors 封装的,底层代码都一样,都是通过创建 ThreadPoolExecutor 对象来完成线程池的构建。

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 ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = (System.getSecurityManager() == null)
? null
: AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
  • corePoolSize:核心池大小,初始化的线程数量
  • maximumPoolSize:线程池最大线程数,它决定了线程池容量的上限

corePoolSize 就是线程池的大小,maximumPoolSize 是一种补救措施,任务量突然增大的时候的一种补救措施。

  • keepAliveTime:线程对象的存活时间
  • unit:线程对象存活时间单位
  • workQueue:等待队列
  • threadFactory:线程工厂,用来创建线程对象
  • handler:拒绝策略

线程池

池化技术 池化思想

优势:

  • 提高线程的利用率
  • 提高响应速度
  • 便于统一管理线程对象
  • 可控制最大并发数

线程池的具体设计思想

  • 核心池的大小
  • 线程池的最大容量
  • 等待队列
  • 拒绝策略

线程池启动的时候会按照核心池的数来创建初始化的线程对象 2 个。

开始分配任务,如果同时来了多个任务, 2 个线程对象都被占用了,第 3 个以及之后的任务进入等待队列,当前有线程完成任务恢复空闲状态的时候,等待队列中的任务获取线程对象。

如果等待队列也占满了,又有新的任务进来,需要去协调,让线程池再创建新的线程对象,但是线程池不可能无限去创建线程对象,一定会有一个最大上限,就是线程池的最大容量。

如果线程池已经达到了最大上限,并且等待队列也占满了,此时如果有新的任务进来,只能选择拒绝,并且需要根据拒绝策略来选择对应的方案。

ThreadPoolExecutor

直接实例化 ThreadPoolExecutor ,实现定制化的线程池,而不推荐使用 Executors 提供的封装好的方法,因为这种方式代码不够灵活,无法实现定制化。

ThreadPoolExecutor 核心参数一共有 7 个

1
2
3
4
5
6
7
8
9
10
11
corePoolSize:核心池的大小
maximumPoolSize:线程池的最大容量
keepAliveTime:线程存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于 corePoolSize,才会生效
TimeUnit:存活时间单位
BlockingQueue:等待队列,存储等待执行的任务
ThreadFactory:线程工厂,用来创建线程对象
RejectedExecutionHandler:拒绝策略
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调用谁处理

单例 1

固定 5

缓存

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

import java.util.concurrent.*;

public class Test {
public static void main(String[] args) {
ExecutorService executorService = null;
try {
/**
* 自己写7大参数,完全定制化
*/
executorService = new ThreadPoolExecutor(
2,
3,
1L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);

for (int i = 0; i < 6; i++) {
executorService.execute(()->{
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"===>办理业务");
});
}

} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}

new ThreadPoolExecutor.AbortPolicy()

new ThreadPoolExecutor.CallersunsPolicy()

new ThreadPoolExecutor.DiscardOldestPolicy()

new ThreadPoolExecutor.DiscardPolicy()

不会抛出异常

线程池 3 大考点:

1、Executors 工具类的 3 种实现

1
2
3
ExecutorService executorService = Executors.newSingleThreadExecutor();
ExecutorService executorService = Executors.newFixedThreadPool(5);
ExecutorService executorService = Executors.newCachedThreadPool();

2、7 个参数

1
2
3
4
5
6
7
corePoolSize:核心池的大小
maximumPoolSize:线程池的最大容量
keepAliveTime:线程存活时间(在没有任务可执行的情况下),必须是线程池中的数量大于 corePoolSize,才会生效
TimeUnit:存活时间单位
BlockingQueue:等待队列,存储等待执行的任务
ThreadFactory:线程工厂,用来创建线程对象
RejectedExecutionHandler:拒绝策略

3、4 种拒绝策略

1
2
3
4
1、AbortPolicy:直接抛出异常
2、DiscardPolicy:放弃任务,不抛出异常
3、DiscardOldestPolicy:尝试与等待队列中最前面的任务去争夺,不抛出异常
4、CallerRunsPolicy:谁调用谁处理

ForkJoin 框架

ForkJoin 是 JDK 1.7 后发布的多线程并发处理框架,功能上和 JUC 类似,JUC 更多时候是使用单个类完成操作,ForkJoin 使用多个类同时完成某项工作,处理上比 JUC 更加丰富,实际开发中使用的场景并不是很多,互联网公司真正有高并发需求的情况才会使用,面试时候会加分

本质上是对线程池的一种的补充,对线程池功能的一种扩展,基于线程池的,它的核心思想就是将一个大型的任务拆分成很多个小任务,分别执行,最终将小任务的结果进行汇总,生成最终的结果。

本质就是把一个线程的任务拆分成多个小任务,然后由多个线程并发执行,最终将结果进行汇总。

比如 A B 两个线程同时还执行,A 的任务比较多,B 的任务相对较少,B 先执行完毕,这时候 B 去帮助 A 完成任务(将 A 的一部分任务拿过来替 A 执行,执行完毕之后再把结果进行汇总),从而提高效率。

工作窃取

ForkJoin 框架,核心是两个类

  • ForkJoinTask (描述任务)
  • ForkJoinPool(线程池)提供多线程并发工作窃取

使用 ForkJoinTask 最重要的就是要搞清楚如何拆分任务,这里用的是递归思想。

1、需要创建一个 ForkJoinTask 任务,ForkJoinTask 是一个抽象类,不能直接创建 ForkJoinTask 的实例化对象,开发者需要自定义一个类,继承 ForkJoinTask 的子类 RecursiveTask ,Recursive 就是递归的意思,该类就提供了实现递归的功能。

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

import java.util.concurrent.RecursiveTask;

/**
* 10亿求和
*/
public class ForkJoinDemo extends RecursiveTask<Long> {

private Long start;
private Long end;
private Long temp = 100_0000L;

public ForkJoinDemo(Long start, Long end) {
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
if((end-start)<temp){
Long sum = 0L;
for (Long i = start; i <= end; i++) {
sum += i;
}
return sum;
}else{
Long avg = (start+end)/2;
ForkJoinDemo task1 = new ForkJoinDemo(start,avg);
task1.fork();
ForkJoinDemo task2 = new ForkJoinDemo(avg,end);
task2.fork();
return task1.join()+task2.join();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.southwind.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;

public class Test {
public static void main(String[] args) {
Long startTime = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoinDemo(0L,10_0000_0000L);
forkJoinPool.execute(task);
Long sum = 0L;
try {
sum = task.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Long endTime = System.currentTimeMillis();
System.out.println(sum+",供耗时"+(endTime-startTime));
}
}

Volatile 关键字

Volatile 是 JVM 提供的轻量级同步机制,可见性,主内存对象线程可见。

一个线程执行完任务之后还,会把变量存回到主内存中,并且从主内存中读取当前最新的值,如果是一个空的任务,则不会重新读取主内存中的值

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

import java.util.concurrent.TimeUnit;

public class Test {
private static int num = 0;

public static void main(String[] args) {
/**
* 循环
*/
new Thread(()->{
while(num == 0){
System.out.println("---Thread---");
}
}).start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

num = 1;
System.out.println(num);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.southwind.demo2;

import java.util.concurrent.TimeUnit;

public class Test {
private static volatile int num = 0;

public static void main(String[] args) {
/**
* 循环
*/
new Thread(()->{
while(num == 0){

}
}).start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

num = 1;
System.out.println(num);
}
}

线程池 workQueue

一个阻塞队列,用来存储等待执行的任务,常用的阻塞队列有以下几种:

  • ArrayBlockingQueue:基于数组的先进先出队列,创建时必须指定大小。
  • LinkedBlockingQueue:基于链表的先进先出队列,创建时可以不指定大小,默认值时 Integer.MAX_VALUE。
  • SynchronousQueue:它不会保持提交的任务,而是直接新建一个线程来执行新来的任务。
  • PriorityBlockingQueue:具有优先级的阻塞队列。

递归

二叉树遍历,深度优先搜索等。

什么是递归?

常规的定义:编程语言中,函数 func 直接或者间接调用函数本身,则该函数称为递归函数。

问前排人是第几排 -> 函数

所有的递归问题都可以用递推公式来表示,所以要用递归解决问题,关键就是先找到递推公式。

1
2
f(n) = f(n-1)+1
f(1) = 1

f(n) 表示你当前是第几排,f(n-1) 前面一排所在的排数,f(1) = 1 表示第一排的人知道自己是第一排。

1
2
3
4
int f(int n){
if(n == 1) return 1;
return f(n-1)+1;
}

递归需要满足 3 要素:

1、一个父问题可以拆分成若干个子问题,并且若干子问题的结果汇总起来就是父问题的答案。

2、父问题和子问题,解题思路必须完全一致,只是数据规模不同。

3、存在终止条件。

问题在不断拆分的同时,一定要在某个节点终止拆分,得到一个明确的答案。

问题:假设有 n 个台阶,每次可以跨 1 个台阶或者 2 个台阶,请问走完这 n 个台阶一共有多少种走法?

1、假设有 1 个台阶,一共有(1) 种走法

2、假设有 2 个台阶,一共有 2 种走法 【1,1】【2】

3、假设有 3 个台阶,一共有()种走法?【1,1,1】【1,2】【2,1】

......

可以根据第一步的走法进行分类

第一类是第一步走了 1 个台阶

第二类是第一步走了 2 个台阶

所以 n 个台阶的走法就等于先走 1 个台阶后,n-1 个台阶的走法+先走 2 个台阶后,n-2 个台阶的走法。

1
f(n) = f(n-1)+f(n-2)

f(1) = 1,能否作为终止条件?

n = 2,f(2) = f(1)+f(0),如果终止条件只有一个 f(1) = 1,f(2) 就无法求解, 因为 f(0) 的值无法确定,

把 f(2) = 2 作为一个终止条件

终止条件有两个:

f(1) = 1;

f(2) = 2;

n = 3,f(3) = f(2)+f(1) = 3

n = 4,f(4) = f(3)+f(2) = 3 + 2 = 5

递推公式

1
2
3
f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2);

推导出递归代码

1
2
3
4
5
int f(int n){
if(n == 1) return 1;
if(n == 2) return 2;
return f(n-1) + f(n-2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.southwind.demo;

public class Test {
public static void main(String[] args) {
for (int i = 1; i <= 30; i++) {
System.out.println(i+"个台阶共有"+f(i)+"种走法");
}
}

public static int f(int n){
if(n == 1) return 1;
if(n == 2) return 2;
return f(n-1) + f(n-2);
}

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

public class Test2 {
public static void main(String[] args) {
int d = f(10);
System.out.println("d:"+d);
}

public static int f(int n){
if(n == 1){
System.out.println("m:"+n);
return 1;
}else{
System.out.println("n:"+n);
//n=4
int c = f(n-1)+1;
System.out.println("c:"+c);
return c;
}
}
}

集合框架

为什么要使用集合框架?

1、数组的长度是固定

2、数组无法同时存储多个不同的数据类型

集合简单理解就是一个长度可以改变,可以保持任意数据类型的动态数组。

集合本身是数据结果的基本概念之一,我们这里说的集合是 Java 语言对这种数据结果的具体实现。

Java 中的集合不是由一个类来完成的,而是由一组接口和类构成了一个框架体系。大致可分为 3 层,最上层是一组接口,继而是接口的实现类。

接口

Collection:集合框架最基础的接口,最顶层的接口。

List:Collection 的子接口,存储有序、不唯一(元素可重复)的对象,最常用的接口。

Set:Collection 的子接口,存储无序、唯一(元素不可重复)的对象。

Map:独立于 Collection 的另外一个接口,最顶层的接口,存储一组键值对象,提供键到值的映射。

Iterator:输出集合元素的接口,一般适用于无序集合,从前往后输出。

ListIterator:Iterator 子接口,可以双向输出集合中的元素。

Enumeration:传统的输出接口,已经被 Iterator 取代。

SortedSet:Set 的子接口,可以对集合中的元素进行排序。

SortedMap:Map 的子接口,可以对集合中的元素进行排序。

Queue:队列接口。

Map.Entry:Map 的内部接口,描述 Map 中存储的一组键值对元素。

Collection 接口

Collection 是集合框架中最基础的父接口,可以存储一组无序,不唯一的对象。

Collection 接口可以存储一组无序,不唯一(可重复)的对象,一般不直接使用该接口,也不能被实例化,只是用来提供规范。

Collection 是 Iterable 接口的子接口。

方法功能
int size()获取集合长度
boolean isEmpty()判断集合是否为空
boolean contains(Object o)判断集合中是否存在某个对象
Iterator<E> iterator()实例化 Iterator 接口,遍历集合
Object[] toArray()将集合转换为一个 Object 数组
T[] toArray(T[] a)将集合转换为一个指定数据类型的数组
boolean add(E e)向集合中添加元素
boolean remove(Object o)从集合中删除元素
boolean containsAll(Collection c)判断集合中是否存在另一个集合的所有元素
boolean addAll(Collection c)向集合中添加某个集合的所有元素
boolean removeAll(Collection c)从集合中删除某个集合的所有元素
void clear()清除集合中的所有元素
boolean equals(Collection c)判断两个集合是否相等
int hashCode()返回集合的哈希值

Collection 子接口

  • List:存放有序、不唯一的元素
  • Set:存放无序、唯一的元素
  • Queue:队列接口

List 接口

List 常用的扩展方法

T get(int index) 通过下标返回集合中对应位置的元素

T set(int index,T element) 在集合中的指定位置存入对象

int indexOf(Object o) 从前向后查找某个对象在集合中的位置

int lastIndexOf(Object o) 从后向前查找某个对象在集合中的位置

ListIterator<E> listIterator() 实例化 ListIterator 接口,用来遍历 List 集合

List<E> subList(int fromIndex,int toIndex) 通过下标截取 List 集合

List 接口的实现类

ArrayList 是开发中使用频率最高的 List 实现类,实现了长度可变的数组,在内存中分配连续空间,所以读取快,增删慢。

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add("Hello");
list.add("World");
list.add("JavaSE");
list.add("JavaME");
list.add("JavaEE");
System.out.println("list:"+list);
System.out.println("list长度:"+list.size());
System.out.println("list是否包含Java:"+list.contains("Java"));
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
list.remove("Hello");
list.remove(0);
System.out.println("******************");
System.out.println(list);
list.add(1,"Spring");
System.out.println(list);
list.add(1,"Spring Boot");
System.out.println(list);
list.set(1,"Spring Cloud");
System.out.println(list);
System.out.println("*************");
System.out.println(list.indexOf("Spring"));
System.out.println(list.subList(1,3));
}
}

List 接口的实现类

ArrayList:基于数组的实现,非线程安全,效率高,所有的方法都没有 synchronized 修饰。

Vector:线程安全,效率低,实现线程安全直接通过 synchronized 修饰方法来完成。

Stack:Vector 的子类,实现了栈的数据结构,(后进先出)

  • push:入栈方法
  • peek:取出栈顶元素,将栈顶复制一份取出,取完之后栈内的数据不变。
  • pop:取出栈顶元素,直接取出栈顶元素,取完之后栈内的数据减一。

LikedList:实现了先进先出的队列,采用链表的形式存储。

ArrayList 和 LikedList 的区别:内存中存储的形式不同,ArrayList 采用的数组的方式,LinkedList 采用的是链表的形式。

数组在内存中存储空间是连续的,读取快,增删慢。

因为数组在内存中是连续的,所以取数据可以通过寻址公式很快求出目标元素的内存地址,因为内存是连续的,所以新增或者删除元素,必然需要移动数据,而且数组长度越长,需要移动的元素越多,操作就越慢。

链表在内存中存储空间是不连续的,读取慢,增删快。链表在内存中是不连续的,没有固定的公式可以使用,要读取只能从第一位开始一直遍历到目标元素,数据规模越大,操作越慢。

增删快,因为只需要重新设置目标元素前后两个节点的后置指针即可,与数据规模无关。

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

import java.util.LinkedList;

public class Test {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add("Hello");
linkedList.add("World");
linkedList.add("Java");
System.out.println(linkedList);
linkedList.offer("JavaSE");
System.out.println(linkedList);
linkedList.push("JavaME");
System.out.println(linkedList);
linkedList.addFirst("First");
System.out.println(linkedList);
linkedList.addLast("Last");
System.out.println(linkedList);
System.out.println(linkedList.peek());
System.out.println(linkedList.peekFirst());
System.out.println(linkedList.peekLast());
System.out.println(linkedList.pop());
System.out.println(linkedList);
}
}

LinkedList 和 Stack 都有 pop 方法,有什么区别和相同点?

pop 方法都是取出集合中的第一个元素,但是两者的顺序是相反的,Stack 是“后进先出”,所以 pop 取出的是最后一个元素,LinkedList 是“先进先出”,所以 pop 取出的是第一个元素。

LinkedList 实现了 Deque 接口,而 Deque 接口是 Queue 的子接口,Queue 就是队列,底层实现了队列的数据结构。

实际开发中,不能直接实例化 Queue 对象。

Queue 的实现类是 AbstractQueue,它是一个抽象类,不能直接实例化,开发中需要实现它的子类 PriorityQueue。

Queue 中添加的数据必须是有顺序的。

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

import java.util.PriorityQueue;

public class Test {
public static void main(String[] args) {
PriorityQueue queue = new PriorityQueue();
// queue.add(1);
// queue.add(2);
// queue.add(3);
// queue.add("a");
// queue.add("b");
// queue.add("c");

queue.add(new A(1));
queue.add(new A(2));
System.out.println(queue);
}
}

class A implements Comparable{

private int num;

public A(int num) {
this.num = num;
}

@Override
public int compareTo(Object o) {
A a = (A)o;
if(this.num > a.num){
return 1;
}else if(this.num == a.num){
return 0;
}else{
return -1;
}
}

@Override
public String toString() {
return "A{" +
"num=" + num +
'}';
}
}

Queue 默认给元素进行升序排列,即自然排序。

Java 集合框架

  • List(有序不唯一)
  • Set
  • Map

List Set:存储的是单个数据,List 可以存储重复的数据,Set 数据不能重复

Map:存储的是一组数据

list.add(1)

list.add(2)

set.add(1)

map.put("name","张三"); key/value

map.put("张三")

map:map 的解释

Set

跟 List 一样,Set 是 Collection 的子接口,Set 集合是以散列的形式存储数据,所以元素是没有顺序的,可以存储一组无序且唯一的数据。

Set 常用实现类:

  • HashSet
  • LinkedHashSet
  • TreeSet

HashSet 是开发中经常使用的一个实现类,存储一组无序且唯一的对象。

无序:元素的存储顺序和遍历顺序不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
HashSet set = new HashSet();
set.add("Hello");
set.add("World");
set.add("Java");
set.add("Hello");
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
set.remove("World");
System.out.println("****************");
iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

LinkedHasSet 是 Set 的另外一个实现类,可以存储一组有序且唯一的元素.

有序:元素的存储顺序和遍历顺序一致。

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

import java.util.Iterator;
import java.util.LinkedHashSet;

public class Test {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add("Hello");
linkedHashSet.add("World");
linkedHashSet.add("Java");
linkedHashSet.add("Hello");
System.out.println("LinkedHashSet的长度是"+linkedHashSet.size());
System.out.println("遍历LinkedHashSet");
Iterator iterator = linkedHashSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
linkedHashSet.remove("Java");
System.out.println(linkedHashSet.contains("Java"));
}
}

equals 和 == 的区别?

所有类中的 equals 都是继承自 Object 类,Object 类中原生的 eqauls 方法就是在通过 == 进行判断

但是每个类都可以对 equals 方法进行重写,覆盖掉之前使用 == 进行判断的逻辑,改用新的逻辑进行判断是否相等。

LinkedHashSet 如何判断两个对象是否相等?

首先会判断两个对象的 hashCode 是否相等

什么是 hashCode?

将对象的内部信息(内存地址、属性值等),通过某种特定规则转换成一个散列值,就是该对象的 hashCode。

  • 两个不同对象的 hashCode 值可能相等。
  • hashCode 不相等的两个对象一定不是同一个对象。

集合在判断两个对象是否相等的时候,会先比较他们的 hashCode,如果 hashCode 不相等,则认为不是同一个对象,可以添加。

如果 hashCode 值相等,还不能认为两个对象是相等的,需要通过 equals 进行进一步的判断,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
55
56
package com.southwind.demo2;

import java.util.Iterator;
import java.util.LinkedHashSet;

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

LinkedHashSet set = new LinkedHashSet();
Data data1 = new Data(1);
set.add(data1);
Data data2 = new Data(1);
set.add(data2);
//是一个对象
System.out.println(data1.equals(data2));
//不是一个对象
System.out.println(set);

}
}

class Data{
private int num;

public Data(int num) {
this.num = num;
}

@Override
public String toString() {
return "Data{" +
"num=" + num +
'}';
}

//hashcode
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
//instanceof 判断对象是否属于某个类
if(obj instanceof Data){
Data data = (Data) obj;
if(this.num == data.num){
return true;
}
}
return false;
}

@Override
public int hashCode() {
return 1;
}
}

==:判断的是栈内存中的值。

引用类型的数据,栈内存中存储的是地址,所以此时 == 判断的是引用地址。

基本数据类型,栈内存中存储的是具体的数值。

栈中存储的是变量

Data data;

int num;

引用类型具体的对象(属性)存储在堆中的,再将堆中对象的内存地址赋值给栈中的变量 data,data 中存储的就是地址。

基本数据类型不需要用到堆内存,变量在栈中,变量的值直接存储在变量中。

TreeSet

LinkedHashSet 和 TreeSet 都是存储一组有序且唯一的数据,但是这里的两个有序是有区别的。

LinkedHashSet 的有序是指元素的存储顺序和遍历顺序是一致的。

6,3,4,5,1,2-->6,3,4,5,1,2

TreeSet 的有序是指集合内部会自动对所有的元素按照升序进行排列,无论存入的顺序是什么,遍历的时候一定按照生序输出。

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

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

public class Test {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
// treeSet.add(1);
// treeSet.add(3);
// treeSet.add(6);
// treeSet.add(2);
// treeSet.add(5);
// treeSet.add(4);
// treeSet.add(1);
treeSet.add("b11");
treeSet.add("e22");
treeSet.add("a33");
treeSet.add("c44");
treeSet.add("d55");
System.out.println("treeSet的长度是"+treeSet.size());
System.out.println("treeSet遍历");
Iterator iterator = treeSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
1
2
3
4
5
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
package com.southwind.demo2;

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

public class Test {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new Data(1));
treeSet.add(new Data(3));
treeSet.add(new Data(6));
treeSet.add(new Data(2));
treeSet.add(new Data(5));
treeSet.add(new Data(4));
treeSet.add(new Data(1));
System.out.println("treeSet的长度"+treeSet.size());
System.out.println("treeSet遍历");
Iterator iterator = treeSet.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}

class Data implements Comparable{
private int num;

public Data(int num) {
this.num = num;
}

/**
* A.compareTo(B)
* 返回值:
* 1 表示A大于B
* 0 表示A等于B
* -1 表示A小于B
* @param o
* @return
*/
@Override
public int compareTo(Object o) {
if(o instanceof Data){
Data data = (Data) o;
if(this.num < data.num){
return 1;
}else if(this.num == data.num){
return 0;
}else{
return -1;
}
}
return 0;
}

@Override
public String toString() {
return "Data{" +
"num=" + num +
'}';
}
}

Map

key-value,数据字典

List、Set 接口都是 Collection 的子接口,Map 接口是与 Collection 完全独立的另外一个体系。

List & Set VS Map

List & Set & Collection 只能操作单个元素,Map 可以操作一对元素,因为 Map 存储结构是 key - value 映射。

Map 接口定义时使用了泛型,并且定义两个泛型 K 和 V,K 表示 key,规定键元素的数据类型,V 表示 value,规定值元素的数据类型。

Map 方法描述
int size()获取集合长度
boolean isEmpty()判断集合是否为空
boolean containsKey(Object key)判断集合中是否存在某个 key
boolean containsValue(Object value)判断集合中是否存在某个 value
V get(Object key)取出集合中 key 对应的 value
V put(K key,V value)向集合中存入一组 key-value 的元素
V remove(Object key)删除集合中 key 对应的 value
void putAll(Map map)向集合中添加另外一个 Map
void clear()清除集合中所有的元素
SetkeySet()取出集合中所有的 key,返回一个 Set
Collectionvalues()取出集合中所有的 value,返回一个 Collection
Set<Map.Entry<K,V>> entrySet()将 Map 以 Set 的形式输出
int hashCode()获取集合的散列值
boolean equals(Object o)比较两个集合是否相等

Map 接口的实现类

  • HashMap:存储一组无序,key 不可以重复,value 可以重复的元素。
  • Hashtable:存储一组无序,key 不可以重复,value 可以重复的元素。
  • TreeMap:存储一组有序,key 不可以重复,value 可以重复的元素,可以按照 key 进行排序。

HashMap 的使用

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

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;

public class Test {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put("h","Hello");
hashMap.put("w","World");
hashMap.put("j","Java");
hashMap.put("s","JavaSE");
hashMap.put("m","JavaME");
hashMap.put("e","JavaEE");
System.out.println(hashMap);
hashMap.remove("e");
System.out.println("删除之后"+hashMap);
hashMap.put("m","Model");
System.out.println("添加之后"+hashMap);
if (hashMap.containsKey("a")){
System.out.println("集合中存在key=a");
}else{
System.out.println("集合中不存在key=a");
}
if(hashMap.containsValue("Java")){
System.out.println("集合中存在value=Java");
}else {
System.out.println("集合中不存在value=Java");
}
Set keys = hashMap.keySet();
System.out.println("集合中的key");
Iterator iterator = keys.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
Collection values = hashMap.values();
for (Object value : values) {
System.out.println(value);
}
System.out.println("************");
iterator = keys.iterator();
while(iterator.hasNext()){
String key = (String) iterator.next();
String value = (String) hashMap.get(key);
System.out.println(key+"-"+value);
}
}
}

Hashtable 用法与 HashMap 基本一样,它们的区别是,Hashtable 是线程安全的,但是性能较低。HashMap 是非线程安全的,但是性能较高。

HashMap,方法没有用 synchronized 修饰,所以是非线程安全的。

Hashtable,方法用 synchronized 修饰,所以是线程安全的。

Hashtable 的使用

1
2
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 com.southwind.demo5;

import java.util.Collection;
import java.util.Hashtable;
import java.util.Set;

public class Test {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put("h","Hello");
hashtable.put("w","World");
hashtable.put("j","Java");
hashtable.put("s","JavaSE");
hashtable.put("m","JavaME");
hashtable.put("e","JavaEE");
System.out.println(hashtable);
hashtable.remove("e");
System.out.println(hashtable);
System.out.println(hashtable.containsKey("a"));
System.out.println(hashtable.containsValue("Java"));
Set keys = hashtable.keySet();
System.out.println(keys);
Collection values = hashtable.values();
System.out.println(values);
}
}

HashMap 和 Hashtable,保存的书画家都是无序的,Map 的另外一个实现类 TreeMap 主要功能是按照 key 对集合中的元素进行排序。

TreeMap 的使用

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

import java.util.Iterator;
import java.util.Set;
import java.util.TreeMap;

public class Test2 {
public static void main(String[] args) {
TreeMap treeMap = new TreeMap();
treeMap.put(new User(3,"Java"),"Java");
treeMap.put(new User(5,"JavaME"),"JavaME");
treeMap.put(new User(1,"Hello"),"Hello");
treeMap.put(new User(6,"JavaEE"),"JavaEE");
treeMap.put(new User(2,"World"),"World");
treeMap.put(new User(4,"JavaSE"),"JavaSE");
System.out.println(treeMap);
Set set = treeMap.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
Object key = iterator.next();
System.out.println(key+"-"+treeMap.get(key));
}
}
}

class User implements Comparable{
private int id;
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public User(int id, String name) {
this.id = id;
this.name = name;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}

@Override
public int compareTo(Object o) {
if (o instanceof User){
User user = (User)o;
if(this.id > user.id){
return 1;
}else if(this.id == user.id){
return 0;
}else {
return -1;
}
}
return 0;
}
}

Collections 工具类

Collection 接口,List 和 Set 的父接口。

Collections 不是接口,它是一个工具类,专门提供了一些对集合的操作,方便开发者去使用,完成相应的业务功能。

Colletions 针对集合的工具类,Collection

Arrays 针对数组的工具类,Array

name描述
public static sort()对集合进行排序
public static int binarySearch(List list,Object v)查找 v 在 list 中的位置,集合必须是生序排列
public static get(List list,int index)返回 list 中 index 位置的值
public static void reverse(List list)对 list 进行反序输出
public static void swap(List list,int i,int j)交换集合中指定位置的两个元素
public static void fill(List list,Object obj)将集合中所有元素替换成 obj
public static Object min(List list)返回集合中的最小值
public static Object max(List list)返回集合中的最大值
public static boolean replaceAll(List list,Object old,Object new)在 list 集合中用 new 替换 old
public static boolean addAll(List list,Object... obj)向集合中添加元素

可变参数,在调用方法的时候,参数可以是任意个数,但是类型必须匹配。

1
2
3
public static void test(int... arg){

}

但是下面这种写法,可以传任意类型,任意数量的参数,多态的一种具体表示形式。

1
2
3
public static void test(Object... arg){

}

Java 中默认输出对象的格式:对象所属的全类名(全限定类名)带着包名的类名+@+对象的哈希值

断点 breakpoint

JavaScript js 脚本语言

1、

2、

3、

Java 是必须全部编译之后,统一执行,假如有 10 行 Java 代码,必须先对这 10 行代码进行编译,通过之后,再交给 JVM 执行。

JS 逐行执行,执行一行算一行,假如有 10 行 JS 代码,一行一行开始执行,执行到第 5 行报错,那么后续 6-10 就不再执行,但是已经执行的前 5 行结果不变。

Java 更加严谨,JS 更加随意

Java 是强语言类型的,JS 是弱语言类型

跟面试官的谈资

1
2
3
4
5
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
package com.southwind.demo;

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

public class Test {
public static void main(String[] args) {
ArrayList list = new ArrayList();
// list.add("Hello");
// list.add("Java");
// Collections.addAll(list,"Java","JavaME","World");
// System.out.println("排序之前");
// System.out.println(list);
//进行排序-》升序a
// Collections.sort(list);
// System.out.println("排序之后");
// System.out.println(list);
//查找元素在集合中的下标,二分查找法(集合中的元素必须升序排列)
// int index = Collections.binarySearch(list,"Java");
// System.out.println("Java 在 list 中的下标"+index);
// System.out.println(list);
// Collections.replaceAll(list,"Java","Collections");
// System.out.println(list);

Collections.addAll(
list,
new User(1,"张三",30),
new User(2,"李四",26),
new User(3,"王五",18)
);

Collections.sort(list);

System.out.println(list);

}
}

class User implements Comparable{
private Integer id;
private String name;
private Integer age;

public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User) o;
if(this.age < user.age){
return 1;
}else if(this.age == user.age){
return 0;
}else{
return -1;
}
}
return 0;
}
}

泛型

泛型(Generics),是指在类定义时不指定类中信息的具体数据类型,而是暂时用一个标识符来替代,当外部实例化对象的时候再来指定具体的数据类型。

1
2
3
4
5
6
7
8
//定义 A 类的时候就指定了属性是 B 类型
public class A{
private B b;

public C test(D d){
return new C();
}
}
1
2
3
4
5
6
7
8
9
//定义 A 类的时候不指定属性的类型
public class A<T,E,M>{
private T b;
public E test(M m){
return E;
}
}

A<B,C,D> a = new A();

优点:这样做极大地提升程序的灵活性,提升类的扩展性,泛型可以指代类中成员变量的数据类型,方法的返回值类型以及方法的参数类型。

泛型的应用

自定义类中添加泛型

1
2
3
4
5
6
public class 类名<泛型1,泛型2,泛型3...>{
private 泛型1 属性名;
public 泛型2 方法名(泛型3){
方法体
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package com.southwind.demo3;

public class Time<T> {
private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
package com.southwind.demo3;

public class Test {
public static void main(String[] args) {
Time<Integer> time1 = new Time<>();
time1.setValue(10);
System.out.println("现在的时间是"+time1.getValue());
Time<String> time2 = new Time<>();
time2.setValue("十点整");
System.out.println("现在的时间是"+time2.getValue());
}
}

泛型用哪个字母都可以,关键是类定义处的字母和类中信息的字母保持一致。

1
2
3
4
5
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 com.southwind.demo4;

public class Time<H,M,S> {
private H hour;
private M minute;
private S second;

public H getHour() {
return hour;
}

public void setHour(H hour) {
this.hour = hour;
}

public M getMinute() {
return minute;
}

public void setMinute(M minute) {
this.minute = minute;
}

public S getSecond() {
return second;
}

public void setSecond(S second) {
this.second = second;
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.southwind.demo4;

public class Test {
public static void main(String[] args) {
Time<String,Integer,Float> time = new Time<>();
time.setHour("十点");
time.setMinute(10);
time.setSecond(10.0f);
System.out.println("现在的时间是"+time.getHour()+":"+time.getMinute()+":"+time.getSecond());
}
}

泛型通配符

有一个参数为 ArrayList 的方法,希望这个方法即可接收泛型是 String 的集合,又可以接收泛型是 Integer 的集合,怎么实现?

多态在泛型中不适用

image-20200326214613663

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {

public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList<>();
ArrayList<Integer> list2 = new ArrayList<>();
test(list1);
test(list2);
}

public static void test(ArrayList<?> list){
System.out.println(list);
}

}

ArrayList<?> 表示可以使用任意的泛型类型对象,这样 test 方法具备通用性了。

泛型上限和下限

上限:表示实例化时具体的数据类型,可以是上限类型的子类或者是上限类型本身,用 extends 表示。

下限:表示实例化时具体的数据类型,可以是下限类型的父类或者是下限类型本身,用 super 表示。

类名<泛型标识 extends 上限类名>

类名<泛型标识 super 下限类名>

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

public static void main(String[] args) {
test(new Time<Float>());
test(new Time<Integer>());
test(new Time<Number>());

test2(new Time<String>());
test2(new Time<Object>());
}

/**
* 泛型上限
* @param time
*/
public static void test(Time<? extends Number> time){

}

/**
* 泛型下限
* @param time
*/
public static void test2(Time<? super String> time) {

}

}

泛型接口

接口

1
2
3
4
5
package com.southwind.demo;

public interface MyInterface<T> {
public T getValue();
}

实现泛型接口有两种方式:

  • 实现类在定义时继续使用泛型标识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.southwind.demo;

public class MyInterfaceImpl<T> implements MyInterface {

private T obj;

public MyInterfaceImpl(T obj) {
this.obj = obj;
}

@Override
public T getValue() {
return this.obj;
}
}
  • 实现类在定义时直接给出具体的数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.southwind.demo;

public class MyInterfaceImpl2 implements MyInterface<String> {

private String obj;

public MyInterfaceImpl2(String obj) {
this.obj = obj;
}

@Override
public String getValue() {
return this.obj;
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.southwind.demo;

public class Test {
public static void main(String[] args) {
MyInterfaceImpl myInterface = new MyInterfaceImpl<String>("接口");
String val = (String) myInterface.getValue();

MyInterfaceImpl2 myInterface1 = new MyInterfaceImpl2("接口");
val = myInterface1.getValue();
}
}

Java 实用类

枚举

枚举 Enum,是一种有确定值区间的数据类型,本质上就是一个类,具有简洁、安全、方便等特点。

枚举的值被约束到了一个特定的范围内,只能从这个范围以内取值。

为什么要有枚举?

因为在描述某些对象的属性时,该属性的值不能随便定义,必须在某个特定的区间内取值。

出于对数据的安全性考虑,类似这种有特定取值范围的数据我们就可以使用枚举来描述。

枚举指由一组常量组成的类型,指定一个取值区间,我们只能从该区间中取值。

1
2
3
4
5
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
final class Week extends Enum{
public static final Week MONDAY;
public static final Week TUESDAY;
public static final Week WEDNSDAY;
public static final Week THURSDAY;
public static final Week FRIDAY;
public static final Week SATURDAY;
public static final Week SUNDAY;
private static final Week $VALUES[];

static{
MONDAY = new Week("MONDAY",0);
TUESDAY = new Week("TUESDAY",1);
WEDNSDAY = new Week("WEDNSDAY",2);
THURSDAY = new Week("THURSDAY",3);
FRIDAY = new Week("FRIDAY",4);
SATURDAY = new Week("SATURDAY",5);
SUNDAY = new Week("SUNDAY",6);
$VALUES[] = (new Week[]{
MONDAY,TUESDAY,WEDNSDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
})
}

public static Week[] values(){
return $VALUES.clone();
}

public static Week valueOf(String s){
return Enum.valueOf(s);
}

private Week(String s,int i){
super(s,i);
}

}

Math

Math 类为开发者提供了一系列的数学方法,同时还提供了两个静态常量 E(自然对数的底数)和 PI(圆周率)。

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

public class Test {
public static void main(String[] args) {
System.out.println("常量E"+Math.E);
System.out.println("常量PI"+Math.PI);
System.out.println("9的平方根"+Math.sqrt(9));
System.out.println("8的立方根"+Math.cbrt(8));
System.out.println("2的3次方"+Math.pow(2,3));
System.out.println("较大值"+Math.max(6.5,1));
System.out.println("-10.3的绝对值"+Math.abs(-10.3));
System.out.println(Math.ceil(10.000001));
System.out.println(Math.floor(10.999999));
System.out.println((int)(Math.random()*10));
System.out.println(Math.rint(5.4));
}
}

Random

用来产生随机数的类,并且可以任意指定一个区间,在此区间范围内产生一个随机数。

方法描述
public Random()创建一个无参的随机数构造器,使用系统时间作为默认种子
public Random(long seed)使用 long 数据类型的种子创建一个随机数构造器
public boolean nextBoolean()返回一个 boolean 类型的随机数
public double nextDouble()返回一个 double 类型的随机数,0.0 - 1.0 之间
public float nextFloat()返回一个 float 类型的随机数,0.0 - 1.0 之间
public int nextInt()返回一个 int 类型的随机数
public int nextInt(n)返回一个 int 类型的随机数,0-n 之间
public long nextLong返回一个 long 类型的随机数,0-1 之间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.southwind.demo5;

import java.util.Random;

public class Test {
public static void main(String[] args) {
Random random = new Random();
//生成订单编号(时间戳+随机数)
for (int i = 1; i <= 10000; i++) {
//随机生成一个六位数
System.out.println("订单"+i+"的编号是:"+System.currentTimeMillis()+random.nextInt(100000)+100000);
}

}
}

String

Java 通过 String 类来创建和操作字符串数据。

  • String 实例化

1、直接赋值

1
String str = "Hello World";

2、通过构造函数创建对象

1
String str = new String("Hello World");

1
2
isLatin1() ? StringLatin1.equals(value, aString.value)
:StringUTF16.equals(value, aString.value);

三目运算符 三元表达式

String 常用方法

方法描述
public String()创建一个空的字符串对象
public String(String value)创建一个值为 value 的字符串对象
public String(char value[])将一个 char 数组转换为字符串对象
public String(char value[],int offset, int count)将一个指定范围的 char 数组转为字符串对象
public String(byte value[])将一个 byte 数组转换为字符串对象
public String(byte value[],int offset, int count)将一个指定范围的 byte 数组转为字符串对象
public int length()获取字符串的长度
public boolean isEmpty()判断字符串是否为空
public char charAt(int index)返回指定下标的字符
public byte[] getBytes()返回字符串对应的 byte 数组
public boolean equals(Object anObject)判断两个字符串值是否相等
public boolean equalsIgnoreCase(Object anObject)判断两个字符串值是否相等(忽略大小写)
public int compareTo(String value)对字符串进行排序
public int compareToIgnoreCase(String value)忽略大小写进行排序
public boolean startsWith(String value)判断字符串是否以 value 开头
public boolean endsWith(String value)判断字符串是否以 value 结尾
public int hashCode()返回字符串的 hash 值
public int indexOf(String str)返回 str 在字符串中的下标
public int indexOf(String str,int formIndex)从指定位置查找字符串的下标
public String subString(int beginIndex)从指定位置开始截取字符串
public String subString(int beginIndex,int endIndex)截取指定区间的字符串
public String concat(String str)追加字符串
public String replaceAll(String o,String n)将字符串中所有的 o 替换成 n
public String[] split(String regex)用指定的字符串对目标进行分割,返回数组
public String toLowerCase()转小写
public String toUpperCase()转大写
public char[] toCharArray()将字符串转为字符数组

null 和空是两个概念。

null 是指对象不存在,引用地址为空。

空是指对象存在,没有内容,长度为零。

StringBuffer

String 对象一旦创建,值不能修改(原来的值不能修改,一旦修改就是一个新的对象,只要一改动,就会创建一个新的对象)

修改之后会重新开辟内存空间来存储新的对象,会修改 String 的引用。

String 的值为什么不能修改?修改之后会创建一个新的对象?而不是在原有对象的基础上进行修改?

因为 String 底层是用数组来存值的,数组长度一旦创建就不可修改,所以导致上述问题。

StringBuffer 可以解决 String 频繁修改造成的空间资源浪费的问题。

StringBuffer 底层也是使用数组来存值。

  • StringBuffer 数组的默认长度为 16,使用无参构造函数来创建对象。

  • 使用有参构造创建对象,数组长度=值的长度+16。

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

public class Test {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer("Hello");
StringBuffer stringBuffer1 = new StringBuffer();
//stringBuffer 底层数组的长度是 21
//stringBuffer1 底层数组的长度是 16
stringBuffer1.append("Hello");
System.out.println(stringBuffer.toString().equals(stringBuffer1.toString()));
System.out.println(stringBuffer.length());
System.out.println(stringBuffer1.length());

}
}

length 方法返回的并不是底层数组的长度,而是它的有效长度(值的长度)

StringBuffer 一旦创建,默认会有 16 个字节的空间去修改,但是一旦追加的字符串长度超过 16,如何处理?

StringBuffer 不会重新开辟一块新的内存区域,而是在原有的基础上进行扩容,通过调用父类 ensureCapacityInternal() 方法对底层数组进行扩容,保持引用不变。

StringBuffer 的常用方法,StringBuffer 是线程安全的,但是效率较低,StringBuilder 是线程不安全的,但是效率较高。

HashMap:线程不安全,效率高

Hashtable:线程安全,效率低

方法描述
public StringBuffer()创建一个空的 StringBuffer 对象
public StringBuffer(String str)创建一个值为 str 的 StringBuffer 对象
public synchronized int length()返回 StringBuffer 的长度
public synchronized char charAt(int index)返回指定位置的字符
public synchronized StringBuffer append(String str)追加内容
public synchronized StringBuffer delete(int start,int end)删除指定区间的值
public synchronized StringBuffer deleteCharAt(int index)删除指定位置的字符
public synchronized StringBuffer replace(int start,int end,String str)将指定区间的值替换成 str
public synchronized String substring(int start)截取字符串从指定位置到结尾
public synchronized String substring(int start,int end)截取字符串从 start 开始,到 end 结束
public synchronized StringBuffer insert(int offset,String str)在指定位置插入 str
public int indexOf(String str)从头开始查找指定字符的位置
public int indexOf(String str,int fromIndex)从 fromIndex 开始查找指定字符的位置
public synchronized StringBuffer reverse()进行反转
public synchronized String toString()转为 String

读取数据不需要考虑线程安全问题,因为这种操作不存在安全隐患。

日期类

  • java.util.Date

Date 对象表示当前的系统时间

  • java.util.Calendar

Calendar 用来完成日期数据的逻辑运算

运算思路:(op+com+t)

1、将日期数据传给 Calendar(Calendar 提供了很多静态常量,专门用来记录日期数据)

常量描述
public static final int YEAR
public static final int MONTH
public static final int DAY_OF_MONTH天,以月为单位
public static final int DAY_OF_YEAR天,以年为单位
public static final int HOUR_OF_DAY小时
public static final int MINUTE分钟
public static final int SECOND
public static final int MILLSECOND毫秒

2、调用相关方法进行运算

方法描述
public static Calendar getInstance()获取 Calendar 实例化对象
public void set(int field,int value)给静态常量赋值
public int get(int field)获取静态常量的值
public final Date getTime()将 Calendar 转为 Date 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.southwind.demo2;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class Test {
public static void main(String[] args) {
//计算今天所在的周是2020年的第几周
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR,2020);
//1月为0,4月为3
calendar.set(Calendar.MONTH,3);
calendar.set(Calendar.DAY_OF_MONTH,9);
int week = calendar.get(Calendar.WEEK_OF_YEAR);
System.out.println(week);
//今天之后的63天是几月几号
int days = calendar.get(Calendar.DAY_OF_YEAR);
days += 63;
calendar.set(Calendar.DAY_OF_YEAR,days);
Date date = calendar.getTime();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
System.out.println(simpleDateFormat.format(date));
//今天之前的63天是几月几号

// calendar.set(Calendar.YEAR,2020);
// //1月为0,4月为3
// calendar.set(Calendar.MONTH,3);
// calendar.set(Calendar.DAY_OF_MONTH,9);

calendar.set(Calendar.DAY_OF_YEAR,100);
calendar.set(Calendar.DAY_OF_YEAR,calendar.get(Calendar.DAY_OF_YEAR)-63);
date = calendar.getTime();
System.out.println(simpleDateFormat.format(date));
}
}

文件

文件

File 类

java.io.File,使用该类的构造函数就可以创建文件对象,将硬盘中的一个具体的文件以 Java 对象的形式来表示。

方法描述
public File(String pathname)根据路径创建对象
public String getName()获取文件名
public String getParent()获取文件所在的目录
public File getParentFile()获取文件所在目录对应的 File 对象
public String getPath()获取文件路径
public boolean exists()判断文件是否存在
public boolean isDirectory()判断对象是否为目录
public boolean isFile()判断对象是否为文件
public long length()获取文件的大小
public boolean createNewFile()根据当前对象创建新文件
public boolean delete()删除对象
public boolean mkdir()根据当前对象创建目录
public boolean renameTo(File file)为已存在的对象重命名

IO

Input 输入流(将外部文件读入到 Java 程序中)

Output 输出流(将 Java 程序中的数据输出到外部)

Java 中的流有很多种不同的分类。

  • 按照方向分,输入流和输出流
  • 按照单位分,可以分为字节流和字符流(字节流是指每次处理数据以字节为单位,字符流是指每次处理数据以字符为单位)
  • 按照功能分,可以分为节点流和处理流。

方法定义时的异常如果直接继承自 Exception,实际调用的时候需要手动处理(捕获异常/丢给虚拟机去处理)

方法定义时的异常如果继承自 RuntimeException,调用的时候不需要处理。

IO 流

  • 按照方向分,可以分为输入流和输出流。
  • 按照单位分,可以分为字节流和字符流。
  • 按照功能分,可以分为节点流和处理流。

字节流

按照方向可以分为输入字节流和输出字节流。

InputStream、OutputStream

1 byte = 8 位二进制数 01010101

InputStream 常用方法

方法描述
int read()以字节为单位读取数据
int read(byte b[])将数据存入 byte 类型的数组中,返回数组中有效数据的长度
int read(byte b[],int off,int len)将数据存入 byte 数组的指定区间内,返回数组长度
byte[] readAllBytes()将所有数据存入 byte 数组并返回
int available()返回当前数据流未读取的数据个数
void close()关闭数据流

OutputStream

方法描述
void write(int b)以字节为单位输出数据
void write(byte b[])将 byte 数组中的数据输出
void write(byte b[],int off,int len)将 byte 数组中指定区间的数据输出
void close()关闭数据流
void flush()将缓冲流中的数据同步到输出流中

字符流

字节流是单位时间内处理一个字节的数据(输入+输出)

字符流是单位时间内处理一个字符的数据(输入+输出)

字符流:

  • 输入字符流 Reader
  • 输出字符流 Writer

Reader

是一个抽象类

Readable 接口的作用?

可以将数据以字符的形式读入到缓冲区

  • 方向:输入+输出
  • 单位:字节+字符
  • 功能:节点流(字节流) + 处理流(对节点流进行处理,生成其他类型的流)

InputStream(字节输入流) ---》 Reader(字符输入流)

InputStreamReader 的功能是将字节输入流转换为字符输入流

英文、数字、符号

1 个字节 = 1 个字符

a 1 个字符、1 个字节

汉字

1 个字符 = 3 个字节

好 1 个字符、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
package com.southwind.demo;

import java.io.FileInputStream;
import java.io.FileReader;
import java.io.InputStream;
import java.io.Reader;

public class Test {
public static void main(String[] args) throws Exception {
//字符流
Reader reader = new FileReader("/Users/southwind/Desktop/test.txt");
int temp = 0;
System.out.println("*******字符流读取********");
while ((temp = reader.read())!=-1){
System.out.println(temp);
}
reader.close();

//字节流
InputStream inputStream = new FileInputStream("/Users/southwind/Desktop/test.txt");
temp = 0;
System.out.println("*******字节流读取********");
while ((temp = inputStream.read())!=-1){
System.out.println(temp);
}
inputStream.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.southwind.demo;

import java.io.FileReader;
import java.io.Reader;

public class Test2 {
public static void main(String[] args) throws Exception {
Reader reader = new FileReader("/Users/southwind/Desktop/test.txt");
char[] chars = new char[8];
int length = reader.read(chars);
System.out.println("数据流的长度是"+length);
System.out.println("遍历数组");
for (char aChar : chars) {
System.out.println(aChar);
}

}
}

read() 返回的是 int ,直接将字符转成字节(1-1,1-3)

read(char[] chars) 返回的是 char 数组,直接就返回字符,不会转成字节的。

Writer

Appendable 接口可以将 char 类型的数据读入到数据缓冲区

OutputStreamWriter 处理流

OutputStreamWriter 的功能是将输出字节流转成输出字符流,与 InputStreamReader 相对应的,将输入字节流转成输入字符流

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

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStream;
import java.io.Writer;

public class Test {
public static void main(String[] args) throws Exception {
Writer writer = new FileWriter("/Users/southwind/Desktop/copy.txt");
//writer.write("你好");
// char[] chars = {'你','好','世','界'};
// writer.write(chars,2,2);
String str = "Hello World,你好世界";
writer.write(str,10,6);
writer.flush();
writer.close();
}
}

处理流

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

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;

public class Test {
public static void main(String[] args) throws Exception {
//基础管道
InputStream inputStream = new FileInputStream("/Users/southwind/Desktop/test.txt");
//处理流
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
char[] chars = new char[1024];
int length = inputStreamReader.read(chars);
inputStreamReader.close();
String result = new String(chars,0,length);
System.out.println(result);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.southwind.demo3;

import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;

public class Test2 {
public static void main(String[] args) throws Exception {
String str = "你好 世界";
OutputStream outputStream = new FileOutputStream("/Users/southwind/Desktop/copy.txt");
OutputStreamWriter writer = new OutputStreamWriter(outputStream);
writer.write(str,2,1);
writer.flush();
writer.close();
}
}

缓冲流

无论是字节流还是字符流,使用的时候都会频繁访问硬盘,对硬盘是一种损伤,同时效率不高,如何解决?

可以使用缓冲流,缓冲流自带缓冲区,可以一次性从硬盘中读取部分数据存入缓冲区,再写入内存,这样就可以有效减少对硬盘的直接访问。

缓冲流属于处理流,如何来区分节点流和处理流?

1、节点流使用的时候可以直接对接到文件对象 File

2、处理流使用的时候不可以直接对接到文件对象 File,必须要建立在字节流的基础上才能创建。

缓冲流又可以分为字节缓冲流和字符缓冲流,按照方向再细分,又可以分为字节输入缓冲流和字节输出缓冲流,以及字符输入缓冲流和字符输出缓冲流。

字节输入缓冲流

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

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;

public class Test {
public static void main(String[] args) throws Exception {
//1、创建节点流
InputStream inputStream = new FileInputStream("/Users/southwind/Desktop/test.txt");
//2、创建缓冲流
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
// int temp = 0;
// while ((temp = bufferedInputStream.read())!=-1){
// System.out.println(temp);
// }

byte[] bytes = new byte[1024];
int length = bufferedInputStream.read(bytes,10,10);
System.out.println(length);
for (byte aByte : bytes) {
System.out.println(aByte);
}

bufferedInputStream.close();
inputStream.close();
}
}

字符输入缓冲流

readLine 方法

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

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.Reader;

public class Test2 {
public static void main(String[] args) throws Exception {
//1、创建字符流(节点流)
Reader reader = new FileReader("/Users/southwind/Desktop/test.txt");
//2、创建缓冲流(处理流)
BufferedReader bufferedReader = new BufferedReader(reader);
String str = null;
int num = 0;
System.out.println("***start***");
while ((str = bufferedReader.readLine())!=null){
System.out.println(str);
num++;
}
System.out.println("***end***,共读取了"+num+"次");
bufferedReader.close();
reader.close();
}
}

字节输出缓冲流

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

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;

public class Test {
public static void main(String[] args) throws Exception {
OutputStream outputStream = new FileOutputStream("/Users/southwind/Desktop/test2.txt");
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
String str = "由于在开发Oak语言时,尚且不存在运行字节码的硬件平台,所以为了在开发时可以对这种语言进行实验研究,他们就在已有的硬件和软件平台基础上,按照自己所指定的规范,用软件建设了一个运行平台,整个系统除了比C++更加简单之外,没有什么大的区别。";
byte[] bytes = str.getBytes();
// for (byte aByte : bytes) {
// bufferedOutputStream.write(aByte);
// }
bufferedOutputStream.write(bytes,9,9);
bufferedOutputStream.flush();
bufferedOutputStream.close();
outputStream.close();
}
}

字符输出缓冲流

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

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.Writer;

public class Test2 {
public static void main(String[] args) throws Exception {
Writer writer = new FileWriter("/Users/southwind/Desktop/test2.txt");
BufferedWriter bufferedWriter = new BufferedWriter(writer);
// String str = "由于在开发语言时尚且不存在运行字节码的硬件平台,所以为了在开发时可以对这种语言进行实验研究,他们就在已有的硬件和软件平台基础上,按照自己所指定的规范,用软件建设了一个运行平台,整个系统除了比C++更加简单之外,没有什么大的区别。";
// bufferedWriter.write(str,5,10);
char[] chars = {'J','a','v','a'};
// bufferedWriter.write(chars,2,1);
bufferedWriter.write(22902);
bufferedWriter.flush();
bufferedWriter.close();
writer.close();
}
}

输入流没有 flush 方法,但不代表它没有缓冲流,输出流是有 flush 方法的,实际开发中在关闭输出缓冲流之前,需要调用 flush 方法。

序列化和反序列化

序列化就是将内存中的对象输出到硬盘文件中保存。

反序列化就是相反的操作,从文件中读取数据并还原成内存中的对象。

序列化

1、实体类需要实现序列化接口,Serializable

1
2
3
4
5
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 com.southwind.entity;

import java.io.Serializable;

public class User implements Serializable {
private Integer id;
private String name;
private Integer age;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}

public User(Integer id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
}

2、实体类对象进行序列化处理,通过数据流写入到文件中,ObjectOutputStream。

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

import com.southwind.entity.User;

import java.io.File;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;

public class Test {
public static void main(String[] args) throws Exception {
User user = new User(1,"张三",22);
OutputStream outputStream = new FileOutputStream("/Users/southwind/Desktop/obj.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
objectOutputStream.writeObject(user);
objectOutputStream.flush();
objectOutputStream.close();
outputStream.close();
}
}

反序列化

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

import com.southwind.entity.User;

import java.io.FileInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;

public class Test2 {
public static void main(String[] args) throws Exception {
InputStream inputStream = new FileInputStream("/Users/southwind/Desktop/obj.txt");
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
User user = (User) objectInputStream.readObject();
System.out.println(user);
objectInputStream.close();
inputStream.close();
}
}

IO 流的应用

IO 流就是完成文件传输(上传文件:发朋友圈、换头像,文件下载:CSDN 下载源代码、文档)

字符 a 你好

文本类型的数据(txt、word、Excel、MD)可以使用字符去读取(当然也可以用字节)

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

import java.io.*;

public class Test3 {
public static void main(String[] args) throws Exception {
Reader reader = new FileReader("/Users/southwind/Desktop/test.txt");
BufferedReader bufferedReader = new BufferedReader(reader);

Writer writer = new FileWriter("/Users/southwind/myjava/test.txt");
BufferedWriter bufferedWriter = new BufferedWriter(writer);

String str = "";
int num = 0;
while ((str = bufferedReader.readLine())!=null){
bufferedWriter.write(str);
num++;
}

System.out.println("传输完毕,共读取了"+num+"次");
bufferedWriter.flush();
bufferedWriter.close();
writer.close();
bufferedReader.close();
reader.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
package com.southwind.demo;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

public class Test {
public static void main(String[] args) throws Exception {
//1、通过输入流将文件读入到 Java
InputStream inputStream = new FileInputStream("/Users/southwind/Desktop/1.png");
//2、通过输出流将文件从 Java 中写入到 myjava
OutputStream outputStream = new FileOutputStream("/Users/southwind/myjava/1.png");
int temp = 0;
int num = 0;
long start = System.currentTimeMillis();
while((temp = inputStream.read())!=-1){
num++;
outputStream.write(temp);
}
long end = System.currentTimeMillis();
System.out.println("传输完毕,共耗时"+(end-start));
outputStream.flush();
outputStream.close();
inputStream.close();
}
}

反射

地位:Java 中最核心的模块,Java 之所以称为动态语言的关键,大部分的类库、企业级框架底层都是通过反射来实现的,非常重要。

反射顾名思义就反转执行,生活中的反射就是通过虚像映射到具体的实物,可以获取到实物的某些形态特征。

程序中的反射,通过一个实例化对象映射到类。

一句话理解反射:常规情况下是通过类来创建对象的,反射就是将这一过程进行反转,通过对象来获取类的信息。

通过对象来获取类的信息

类的信息我们也同样使用对象来描述,Class 类专门用来描述其他类的类,每一个 Class 的实例化对象都是对某个类的描述。

Class 是反射的源头

如何来创建 Class 的对象?

1、调用 Class 的静态方法 forName(String name),将目标类的全限定类名(全类名,带着包名的类名)

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

public class Test {
public static void main(String[] args) throws Exception {
User user = new User();
Class clazz = Class.forName("com.southwind.demo2.User");
System.out.println(clazz.getName());
System.out.println(clazz.getTypeName());
System.out.println(clazz.getSuperclass().getName());
Class[] array = clazz.getInterfaces();
System.out.println("****************");
for (Class aClass : array) {
System.out.println(aClass);
}
}
}

2、通过目标类的 class 创建,Java 中的每一个类都可以调用类.class,class 不是属性也不是方法,叫做“类字面量”,作用是获取内存中目标类型对象的引用(类的结构)。

1
2
Class clazz2 = User.class;
System.out.println(clazz2.getName());

3、通过目标类的实例化对象获取,getClass()

1
2
Class clazz3 = user.getClass();
System.out.println(clazz3.getName());

内部类

  1. 成员内部类
  2. 局部内部类
  3. 匿名内部类
  4. 静态内部类
1
https://www.cnblogs.com/dolphin0520/p/3811445.html