Skip to content

初识Java

1.1 计算机的组成

  • 硬件:可以看得见,摸得着的物理设备。
  • 软件:软件是一个相对概念,软件看得见,摸不着

1.2 软件

软件(程序)是实现了特定功能的指令集合。软件是通过计算机编程语言进行实现的。

1.3 编程语言

常见语言包含:C、C++、C#、Python、Java…….

1.4 Java语言

1.4.1 概述

Java是一种面向对象的高级语言。他具有简单易学等特点被广泛应用。

1.4.2 特点

  • 面相对象
  • 多线程
  • 自动垃圾回收
  • 跨平台
  • 支持分布式
  • ……

1.4.3 技术方向

  • Java SE(Java Standard Edition):Java标准版,Java基础知识,如:数据类型、分支语句与循环、数组等
  • Java EE(Java Enterpise Edition):Java企业级版,实现Java企业级开发。如:Jsp、SSM框架等
  • Java ME(Java Micro Edition):Java微小版,支持嵌入式开发

2 安装Jdk

2.1 下载与安装

2.2 配置环境变量

2.3 概念

  • JVM(Java Virtual Machine)‌:JVM是Java虚拟机,是Java程序运行的环境。它负责解释和执行Java字节码,使其能够在不同的操作系统和硬件平台上运行。JVM是跨平台的基础。
  • JRE(Java Runtime Environment)‌:JRE是Java运行时环境,包含了JVM和Java基础类库(JRE=JVM + 类库)
  • JDK:JDK(Java Development Kit)‌:JDK是Java开发工具包,包含了JRE和开发工具。它为Java开发者提供了一个完整的开发环境,包括编译器、调试器、类库等,是开发Java应用程序的基础平台(JDK=JRE+工具包)

3 第一个Java程序

3.1 Dos命令

3.1.1 命令行窗口

  • 运行==》cmd
  • Windows键+R==》cmd

3.1.2 常用Dos命令

  • Dir:列出当前目录下所有子目录及文件
  • Cd:进入下一级目录
  • Cd ..:回到上一级目录
  • Cd \:回到根目录
  • Md:新建子目录
  • Rmdir:删除指定名称的子目录
  • Ipconfig:查看当前本机网络配置
  • Ping:测试网络是否可以连通

3.2 第一个Java程序

3.2.1 步骤

  1. 编写源代码
  • 创建文本文件并以.java为后缀进行命名
  • 编写源代码
  1. 编译源代码
  • 使用Javac命令对源代码进行编译,编译后生成字节码(.class)文件
  1. 运行代码
  • 使用java命令对字节码(.class)程序进行执行

3.2.2 代码

java
public class Demo01{
	public static void main(String[] a){
		System.out.println("Hello World");
	}
}

3.2.3 说明

  1. 代码框架
java
public class Demo01{}

说明:

  • Public:访问修饰符,用于限制类的可访问范围
  • Class:定义类的关键字
  • Demo01:类名,自定义名称。

注意:在一个.java文件中可以包含多个类,但被public修饰的类只能有一个。如果类用public修饰符进行修饰,则类名必须和文件名保持一致。

  1. 入口函数
java
public static void main(String[] a){}

说明: Java程序默认都是从main()方法开始进行执行的,所以main()方法也被称为程序的入口函数。

四要素:

  • Public修饰符
  • Static关键字
  • Void返回类型
  • String[]类型参数

函数体: 任意有效的Java代码

  1. 输出语句

输出语句:

  • 换行输出:System.out.println([内容])
  • 非换行输出:System.out.print(内容)

区别: 换行输出在内容输出后,输出内容的末尾加换行符。非换行输出在内容输出后不加换行符。直到一行的末尾将自动换行。

3.2.4 注意事项

  • Java代码区分大小写
  • 代码文件中可以包含多个类,但只能有一个类被public修饰符修饰,且类名必须和文件名保持一致
  • 文件的命名需要符合命名规范
  • 类名采用有意义的英文单词进行命名
  • 类名的首字母要大写
  • 编码时注意编码规范
  • 代码要合理缩进
  • 合理使用注释语句
  • 每行代码都已”;”进行结束

3.3 注释语句

3.3.1 作用

注释语句通常用于完成对代码的解释说明,注释语句不会被编译器所编译执行。

3.3.2 注释语句

  1. 单行注释
  • //注释内容:每次只能注释1行代码
  1. 多行注释
  • /注释内容/:一次可以注释多行代码
  1. 文档注释
  • /**注释内容*/:用于对类或方法进行注释,文档注释在生成文档时会出现在文档中。

4 变量与数据类型

程序的运行是在内存中完成的。如何合理的利用内存?

4.1 数据类型

4.1.1 Java中的内存

  • 栈内存:栈内存空间相对较小,具有较高的执行效率。栈内存的分配与释放是自动完成
  • 堆内存:堆内存空间相对较大,执行效率相对较低。堆内存空间的分配需要通过new操作符完成,通过GC进行回收

4.1.2 数据长度单位

  • Bit(位):二进制中的一个1或0就称为1Bit
  • Byte(字节):1字节=8位,在计算机中1字节可以存储任何的字符及符号。如果存储中文则需要2个字节
  • KB:1Kb=1024Byte
  • MB:1Mb=1024Kb

4.1.3 数据类型

  1. 基本数据类型

基本类型一般存储在栈内存空间中。

类型长度说明
Byte1取值范围:-128~127
Short2
Int4默认类型
Long8
Float4数值末尾+f/F
Double8默认类型
Char2可以存储任何类型数据,数据需要用’’引起来
Boolean1用于表示真和假,取值包含:true和false
  1. 引用类型

引用类型数据存储在堆内存空间中。引用类型的大小是不固定。如:String、数组等。

4.2 变量

4.2.1 概述

变量的本质是一块内存空间。这块内存空间中的数据会随程序的执行而发生改变,所以将这块内存称为变量。

4.2.2 定义语法

java
数据类型 变量名 [=值];

4.2.3 示例

java
public class Demo03{
	public static void main(String[] a){
		//1、先声明,后赋值
		//声明变量
		byte b1;
		//变量赋值
		b1 = 20;

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

		//2、声明并赋值
		byte b2 = 77;
		System.out.println("b2="+b2);

		//3、声明布尔类型变量
		boolean b = true;
		System.out.println("b="+b);

		//4、定义字符类型变量
		char c = '男';
		System.out.println("c="+c);

	}
}

4.2.4 变量命名规范

  • 变量名以字母、数字、_及$组成,且首字母不能为数字。如:_name、h2
  • 不能使用关键字及保留字
  • 变量的命名采用camel命名法进行命名
  • 变量由一个单词组成,则单词全部小写。如:name、age
  • 变量由多个单词组成,则第一个单词全部小写,其后各单词首字母大写。如:userName、stuSchoolName等

4.2.5 注意

  • 变量使用前必须进行初始化
  • 同一个代码块中该变量不能重复定义

4.2.6 变量的本质

  • 变量的本质是一块内存空间
  • 编译器根据数据类型来分配空间的大小
  • 在计算机底层通过内存地址来匹配内存空间,但内存地址不便于操作。所以语言中通过设置别名(变量名)的方式来实现地址的查找
  • 每个变量的内存空间大小分配后是无法再次改变的,但是空间中的数据是可以随程序的运行而发生改变的

4.3 练习

  1. 定义两个整数类型变量并进行复制,输出两个变量相加之和。

5 Scanner类

5.1 类

java
java.util.Scanner

5.2 作用

接收用户的键盘输入,并解析成特定类型的数据。

5.3 常用方法

  • next():扫描下一个输入并解析为字符串类型数据
  • nextXxx():扫描下一个输入并解析为Xxx类型数据

5.4 示例

java
//1、导包(说明需要使用类的所在位置)
import java.util.Scanner;

public class Demo07{
	public static void main(String[] a){
		//2、创建(实例化)Scanner对象
		Scanner sc =new Scanner(System.in);

		//3、使用Scanner对象
		int age;
		System.out.print("请输入您的年龄:");
		age=sc.nextInt();

		System.out.println("您的年龄为:"+age);		
	}
}

5.5 练习

  1. 从键盘接收学生的信息,包含:姓名、性别、年龄、电话号码。然后按照特定格式进行输出。输入格式为:

学生基本信息:

姓名:Xxx

年龄:xx岁

性别:xx

电话号码:xxxxx

6 运算符

运算符就是实现各种运算的符号。

6.1 算术运算符

6.1.1 运算符

+、-、*、/、%

6.1.2 作用

对操作数执行算术运行。

6.1.3 示例

java
/**
* 算术运算符
*/
public class Demo09{
	public static void main(String[] a){
		//1、除法运算
		//除运算
		int i1 = 20;
		int i2 = 5;
		int i3 = 7;
		float i4 = 5.0f;

		//a、可以整除:运算结果为整数
		System.out.println(i1/i2);
		//b、无法整除:如果两个运算数均为整数类型,则结果为整数
		System.out.println(i1/i3);
		//c、其中一个运算数为浮点数:如果其中一个运算数为浮点数,则结果为浮点数
		//运算过程中编译器会将另一个运算数转换为浮点数然后在进行运算
		System.out.println(i1/i4);

		//2、取余运算:计算两个数相除的余数
		System.out.println(i1%i2);
		System.out.println(i1%i3);
		System.out.println(i1%i4);

		//3、+运算符
		//+运算符遇到字符串类型数据则执行字符串拼接操作。运算结果为String类型数据
		System.out.println(i1+"Hello");
		System.out.println(2+3+"a"+7);
	}
}

6.1.4 注意

  • 如果参与运算的两个运算数均为整数类型,则运行结果为整数类型,如果其中一个运算数为浮点数类型,则运算结果为浮点类型
  • +运算符执行运算过程中,遇到字符串类型数据则开始执行拼接操作。最终运算结果也为String类型数据

6.2 一元运算符

6.2.1 运算符

++、--

6.2.2 作用

对变量自身执行±1运算。

6.2.3 示例

java
/**
* 一元运算符
*/
public class Demo01{
	public static void main(String[] a){
		/*
		//实现对一个变量的自身执行+1运算
		int i =20;
		i = i+1;

		//使用一元运算符进行运算
		int i=1;
		i++;
		System.out.println(i);
		*/

		int j = 1;
		//前自加:首先执行对变量自身的+1运算,然后在执行其他运算
		System.out.println(++j);

		//相当于
		j = j+1;
		System.out.println(j);

		//后自加:先执行其他运算,然后在对变量自身执行+1运算
		System.out.println(j++);
		System.out.println(j);

		//相当于
		System.out.println(j);
		j=j+1;
	}
}

6.2.4 说明

  • 运算符在变量前则先执行对变量自身的±1运算,然后再执行其他操作
  • 运算符在变量的后面则先执行其他操作,然后在执行变量自身的±1运算

6.3 关系运算符

6.3.1 运算符

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

6.3.2 作用

用于比较两个运算数之间大小或相等关系的运算符。

6.3.3 示例

java
/**
* 关系运算符
*/
public class Demo02{
	public static void main(String[] a){
		int i=20,j =34;

		//1、数值类型运算数
		//比较两个运算数之间的大小关系
		System.out.println(i>j);
		//比较两个运算数是否相等
		System.out.println(i==j);

		//2、布尔类型运算数
		//执行错误
		//System.out.println(true>false);
		//boolean类型数据只能执行相等或不等的比较
		System.out.println(true==false);

		//3、String类型运算数
		//执行错误
		//System.out.println("abc">"def");
		//String类型数据只能执行相等或不等运算
		System.out.println("abc" == "def");

	}
}

6.3.4 注意

  • String、boolean等类型运算数无法执行大小的比较,只能执行相等或不等比较

6.4 逻辑运算符

6.4.1 运算符

&&、||、!

6.4.2 作用

逻辑运算符主要用于连接两个关系表达式(或布尔类型数据)并进行运算的运算符。逻辑运算符的运算结果为boolean类型数据。

6.4.3 示例

java
/**
* 逻辑运算符
*/
public class Demo03{
	public static void main(String[] a){
		//1、逻辑与运算:遇到false则整个表达式的结果即为false
		//表达式1的结果为false,表达式2的结果为true
		//表达式的最终结果为false
		System.out.println(2>3 && 3<4);
		//表达式1的结果为true,表达式2的结果为true
		//表达式的最终结果为true
		System.out.println(2<3 && 3<4);

		//2、逻辑或运算:遇到true则整个表达式的结果即为true
		//表达式1的结果为false,表达式2的结果为true
		//表达式的最终结果为true
		System.out.println(2>3 || 3<4);
		//表达式1的结果为true,表达式2的结果为true
		//表达式的最终结果为true
		System.out.println(2<3 || 3<4);


		//3、逻辑非运算:对表达式的值进行取反操作
		System.out.println(!true);
		System.out.println(!false);
	}
}

6.5 位运算符

6.5.1 运算符

&、|、^、<<、>>

6.5.2 作用

对操作数按位执行运算。

6.5.3 示例

java
/**
* 位运算符
*/
public class Demo04{
	public static void main(String[] a){
		int i = 4,j=6;
		//1、位与运算:将运算数转为二进制,然后按位执行与运算
		System.out.println(i & j);
		//2、位或运算:将运算数与转为二进制,然后按位执行或运算
		System.out.println(i | j);

		//3、(左)移位运算:左移乘
		System.out.println(i<<2);
		//4、(右)移位运算:右移除
		System.out.println(i>>1);
	}
}

6.6 三元运算符

6.6.1 运算符

(表达式1) ? 表达式2 : 表达式3

6.6.2 示例

java
/**
* 三元运算符
*/
public class Demo05{
	public static void main(String[] a){
		//三元运算符首先执行表达式的判断。
		//如果表达式的结果为true则返回表达式2的值,否则将返回表达式3的值
		System.out.println(2>3 ? "Yes" : "No");
	}
}

6.7 赋值运算符

6.7.1 运算符

=

简写:+=、-=、*=、/=、%=

6.7.2 作用

将表达式的运算结果赋值给变量。

6.7.3 示例

java
/**
* 赋值运算符
*/
public class Demo06{
	public static void main(String[] a){
		int i = 2;
		//int j = j+2;
		int j+=2;		
	}
}

6.7.4 说明

  • 简化的赋值运算符在运算过程中会隐式执行类型强制转换操作。

6.8 运算符的优先级

  • ()、[]、.
  • ++、--
  • *、/、%
  • +、-
  • <<、>>
  • <、<=、>、>=
  • ==、!=
  • &&、||、!
  • =、+=、-=、*=、/=、%=

6.9 练习

  1. 键盘接收长方形的长和宽,编写代码计算长方形的周长和面积
  2. 键盘接收任意一个三位整数,分别输出这个三位数的百、十、个位上的数是多少
  3. 键盘任意输入两个输,输出其中较大的数字的值。

7 分支语句

7.1 作用

根据条件表达式控制程序代码的执行。

7.2 If…else语句

7.2.1 语法

java
If(条件表达式){
 [代码段1]
}
[
else [if(条件表达式2)]{
 [代码段2]
}
]

7.2.2 示例

  1. 单分支if语句
java
public class Demo07{
	//如果年级满18岁则输出"可以入伍"
	public static void main(String[] a){
		Scanner sc = new Scanner(System.in);

		//提示并接收用户的输入
		System.out.print("年龄:");
		int age = sc.nextInt();

		//使用if分支语句执行判断操作
		if(age>=18)
			System.out.println("年龄满足入伍条件,请准备参加体检");	
	}
}
  1. If...else语句
java
public class Demo08{
	//如果年级满18岁则输出"可以入伍",否则输出"年龄不符合入伍要求"
	public static void main(String[] a){
		Scanner sc = new Scanner(System.in);

		//提示并接收年龄信息
		System.out.print("请输入年龄:");
		int age = sc.nextInt();

		//判断年龄是否满足入伍要求
		if(age >=18)
			System.out.println("您已满足入伍要求,请准备参加体检");
		else
			System.out.println("抱歉,您的年龄不满足入伍要求");	

	}
}
  1. 多分支if语句
java
/**
* 多分支if语句
*/
public class Demo09{
	//需求:将整数转为对应的字符。如:1==A,2==B、3==C
	public static void main(String[] a){
		Scanner sc= new Scanner(System.in);

		//提示并接收用户的键盘输入
		System.out.print("请输入一个整数:");
		int num = sc.nextInt();

		if(num==1)
			System.out.println("A");
		else if(num==2)
			System.out.println("B");
		else if(num==3)
			System.out.println("C");
		else
			System.out.println("Other");

	}
}
  1. 说明
  • 如果代码段只有1条语句,可以省略{}
  • 多分支if语句执行过程中,只能执行其中一个语句段

7.3 Switch语句

7.3.1 语法

java
Switch(表达式){
Case 值1:
 [代码段1]
 [break;]
Case 值2:
 [代码段2]
 [break;]
……
Default:
 [代码段n+1]
 [break;]
}

7.3.2 示例

java
public class Demo10{
	//需求:将整数转为对应的字符。如:1==A,2==B、3==C
	public static void main(String[] a){
		Scanner sc = new Scanner(System.in);

		//提示用户输入一个整数
		System.out.print("请输入一个整数:");
		int num =sc.nextInt();

		switch(num){
			case 1:
				System.out.println("A");
				break;
			case 2:
				System.out.println("B");
				break;
			case 3:
				System.out.println("C");
				break;
			default:
				System.out.println("other");
				break;
		}
	}
}

7.3.3 说明

  • Switch后表达式的类型只支持byte、short、int、char、enum。Jdk1.8后支持String
  • Default的执行顺序与位置无关
  • 如果缺省了break语句,代码将被贯穿执行。直到遇到break语句为止

7.3.4 Break语句

Break语句用于在case语句中跳出case语句段。

7.3.5 If语句与case语句

  • If语句的条件可以是任意的表达式;switch语句的条件有类型限制
  • If语句的条件可以使用区间;switch语句的条件是固定的值

7.4 练习

  1. 男孩6岁可以搬动桌子,女孩7岁可以搬动桌子。键盘获取学生信息,并输出是否可以搬动桌子
  2. 公园按照不同年龄段收取门票,票价规则为:
  • 旺季(5~10):成人50,学生25,儿童(1~7)免门票,老人(55)免门票
  • 淡季:成人25,学生12,儿童(1~7)免门票,老人(55)免门票
  1. 学校食堂每日午餐菜谱如下:
  • 周一、周三:面条
  • 周二、周五:馒头
  • 周四:水饺
  • 周六、日:休息

编写代码根据星期输出本日主食

  1. 编写代码使用银行储户管理系统的菜单功能,基本要求如下:
  • 操作员成功登录显式系统菜单,菜单内容如下:

欢迎登录储户管理系统

  1. 新增储户信息

  2. 查询账号余额

  3. 修改密码

  4. 取款

  5. 、 退出

  • 如果登录失败则进行提示,连续错误三次将提示“账号已被冻结”
  • 操作员默认账号为a0661,密码为123456
  • 实现取款功能,当金额不足时取款失败

8 循环语句

8.1 概述

重复或反复执行某个动作的过程称为循环。如:同学在操场跑步、打印机在打印内容。

8.2 While循环

8.2.1 语法

java
while(条件表达式){
 [循环体]
}

8.2.2 示例

java
/**
* while循环示例
*/
public class Demo12{
	public static void main(String[] a){
		//定义变量记录循环次数(循环变量)
		int times =0;

		while(times<99){
			System.out.println(times+"、对不起,我错误了");

			times++;
		}
	}
}

8.2.3 执行过程

执行过程:
循环语句首先判断循环条件,如果条件表达式为true则执行循环体。否则将跳过循环体,循环执行结束
特点:先判断,后执行

8.2.4 注意

  • 在循环过程中需要进行循环变量迭代,否则循环将成为死循环

8.3 Do…while循环

8.3.1 语法

java
do{
 [循环体]
}while(条件表达式);

8.3.2 示例

java
/**
* do...while循环
*/
public class Demo13{
	public static void main(String[] a){
		int times =0;

		do{
			System.out.println(times+"、对不起,我错误了");
			times++;
		}while(times<99);
	}
}

8.3.3 执行过程

执行过程:
Do…while循环首先执行循环体,循环体执行结束后执行条件表达式的判断,如果条件表达式的结果为true则在再次执行循环体,直到循环条件不成立为止。
特点:先执行,后判断,至少执行1次

8.3.4 注意

  • While语句后面的”;”不能缺省,缺省将产生语法错误

8.4 For循环

8.4.1 语法

java
for(循环变量初始化;条件判断;循环变量迭代){
 [循环体]
}

8.4.2 示例

java
/**
* for循环使用示例
*/
public class Demo14{
	public static void main(String[] a){
		for(int times =0;times<99;times++){
			System.out.println("对不起,我错了");
		}
	}
}

8.4.3 执行过程

与while相同

8.5 对比

特点最小执行次数应用
While循环先判断,后执行0循环次数未知
Do…while循环先执行,后判断1循环次数未知
For循环先判断,后执行0循环次数已知

8.6 跳转语句

8.6.1 作用

控制代码的执行,跳转语句会影响循环语句的执行过程。

  1. Continue
java
public class Demo01{
	public static void main(String[] a){
		for(int i = 1;i<=10;i++){
			if(i%3==0)
				continue;

			System.out.print(i+"\t");
		}
	}
}

作用:终止本次循环,开始下一次循环。

  1. Break
java
public class Demo02{
	public static void main(String[] a){
		for(int i = 1;i<=10;i++){
			if(i%3==0)
				break;

			System.out.print(i+"\t");
		}
	}
}

作用:跳出当前循环

8.7 嵌套循环

8.7.1 概述

所谓嵌套循环语句就是在一个循环语句的内部定义了另一个循环语句。

8.7.2 示例

java
public class Demo03{
	public static void main(String[] a){
		//外层循环
		for(int i =1;i<=3;i++){
			//内层循环
			for(int j = 1;j<=2;j++){
				System.out.println("i="+i+"\tj="+j);
			}
		}
	}
}

8.7.3 执行过程

  • 循环从外循环开始进行执行,外循环执行后将进入到循环体
  • 外循环的循环体同样是一个(内)循环,内循环将完整进行执行。内循环执行结束后再次回到外循环

8.7.4 循环跳转

Break语句只能中断所在循环的正常执行,不会影响其他循环的执行。

8.7.5 建议

嵌套循环尽量不要大于3层。否则可能会影响程序的执行效率。

8.8 练习

  1. 编写代码计算50~100之间所有偶数之和,并输出计算结果
  2. 编写代码输出如下结果:

**

**

**

**

  1. 编写代码输出如下结果:

**



  1. 编写代码输出如下结果:


**

  1. 编写代码输出如下效果



  1. 打印九九乘法表

  1. 鸡兔同笼共80个头,208只脚,鸡和兔各有几只

9 数组

9.1 为什么使用数组?

存储西游记中师徒4人的名字,用变量就可以。如果需要存储多西游记中所有出现的人物,使用变量就不合适了。因为太多变量不便于管理和使用。

9.2 概述

数组是相同类型数据的集合。数组是一块连续的内存空间。数组的长度是固定的。

9.3 一维数组

9.3.1 语法

  1. 动态初始化
java
数据类型[] 数组名 = new 数据类型[长度];
数据类型 数组名[] = new 数据类型[长度];
  1. 静态初始化
java
数据类型[] 数组名 = new 数据类型[]{元素列表}

9.3.2 说明

  • new:操作符,用于向内存申请空间

9.3.3 示例

java
public class Demo04{
	public static void main(String[] a){
		//1、动态实例化
		//声明数组对象
		//先声明,后实例化
		int[] arr1;
		arr1 = new int[3];

		//声明并实例化
		int[] arr2 = new int[3];

		//2、静态实例化
		int[] arr3 = new int[]{2,7,33};
	}
}

9.3.4 注意

  • 静态实例化数组对象时一定不能指定数组的长度(由编译器根据元素的个数进行推断)
  • 静态实例化数组对象时可以省略new 数据类型[]
  • 不同类型数组对象的元素的初始值也是不同的:
  • 整数类型:0
  • 浮点数类型:0.0
  • Boolean:false
  • Char:\u0000
  • 引用类型:null

9.3.5 使用一维数组

  1. 语法
java
数组名[索引下标]
  1. 说明
  • 数组索引下标从0开始
  • 数组的最大索引下标=数组长度-1
  1. 示例
java
public class Demo05{
	public static void main(String[] a){
		//定义数组对象
		int[] arr = new int[3];

		//为数组对象的元素进行赋值
		arr[0] = 23;
		arr[1] = 77;

		//读取数组对象的元素值
		System.out.println(arr[0]);
		System.out.println(arr[2]);
	}
}

在栈内存中存储了数组对象的首地址,在使用数组对象时为了更快速的计算出目标元素的存储地址,所以使用了地址偏移的计算方式进行实现。从0开始就可以得到数组对象的首地址。所以索引从0开始。

  1. 注意
  • 如果使用的索引值超出合法的范围内将产生异常:ArrayIndex OutOfBounds(数组索引下标越界)

9.3.6 遍历数组的元素

  1. 循环语句
  • For循环
  • 增强for循环(for…in)
  1. 示例
java
public class Demo06{
	public static void main(String[] a){
		int[] arr = {2,87,3,19,66,51};

		//通过for循环遍历数组元素
		//可以读取数组对象的元素值,也可以修改数组对象的元素值
		for(int i =0;i<6;i++){
			//System.out.println(arr[i]);
			arr[i] = arr[i]+18;
		}


		//通过增强for循环遍历数组元素
		//只能读取数组的元素值,无法通过循环变量修改元素值
		//为什么修改无效:因为变量i只是一个临时变量,数组的元素值被存储到了变量i中,
		//对变量i的操作并不会影响到数组的元素
		for(int i : arr){
			//System.out.println(i);
			i = i+18;
		}


		//打印修改后的元素值
		for(int i : arr){
			System.out.println(i);
		}
	}
}

9.3.7 Length属性

  1. 作用

返回当前数组对象的容量。

  1. 示例
java
public class Demo07{
	public static void main(String[] a){
		int[] arr = {2,8,54,63};

		System.out.println("len="+ arr.length);

		//应用length属性
		for(int i =0;i<arr.length;i++){
			System.out.println(arr[i]);
		}
	}
}

9.4 多维数组

9.4.1 概述

我们通常将二维及二维以上的数组统称为多维数组。多维数组一般也称为数组的数组。

9.4.2 语法

  1. 动态初始化
java
数据类型[][] 数组名 = new数据类型[长度1][长度2]
  1. 静态初始化
java
数据类型[][] 数组名 = new 数据类型{{元素列表1},{元素列表2}……}

9.4.3 示例

java
public class Demo08{
	public static void main(String[] a){
		//定义多维数组
		//动态初始化
		int[][] arr1 = new int[2][2];

		//静态初始化
		int[][] arr2 = {{2,7},{3,9}};

		//交错数组/锯型数组
 		int[][] arr3 = new int[2][];
	}
}

9.4.4 使用多维数组

java
public class Demo09{
	public static void main(String[] a){
		int[][] arr = new int[2][3];

		arr[1][0] = 28;
		arr[1][2] = 39;
	}
}

9.5 Arrays工具类

9.5.1 概述

工具类,类内部提供了一组用于操作数组对象的静态方法。

9.5.2 常用方法

  • Fill():使用指定值对数组对象进行填充
  • hashCode():返回数组对象的哈希码值
  • equals():比较两个数组对象是否相等
  • copyOf():复制数组对象并指定数组的新容量,可用于进行扩容操作
  • sort():对数组元素对象进行排序
  • binarySearch():使用二分查找法查找数组中的元素
  • toString():将数组对象转为字符串对象

9.6 练习

  1. 定义一个具有n个元素的数组对象,输出数组对象中最大及最小元素值
  2. 定义一个具有n个元素值的数组对象,使用不同的方式实现数组元素的排序(升序)。
  • 选择排序算法

实现思路:从第一个元素开始进行比较,首先被选定元素与其后每一个元素逐一进行比较。然后按照排序规则进行交换。一轮比较结束后首先确定第一个元素的值,后面的每一个元素按照这样规则继续进行比较,直到最后一个元素为止。

  • 选择排序算法

实现思路:相邻两个元素进行比较。每次比较后会有一个满足条件的元素升上去。经过n-1次比较后将得到比较结果。

  1. 定义一个具有n个元素的数组对象,使用不同的方式实现元素的查找操作,并返回元素在数组中的索引位置。
  • 思路1

注意遍历元素,然后对当前元素值与待查找值进行比较。如果相等则返回索引位置,否则继续向下查找。直到所有元素比较结束。

  • 实现思路:

首先找到有序数组的中间位置,然后用待查找值与中间位置的元素值进行比较,如果相等则匹配成功,否则根据值在数组中的大致范围找到下一个查找区域在当前元素的左边还是右边,然后再从中间位置进行查找。该过程一直重复到元素查找成功或失败。

  1. 对于一个长度为n的数组对象,数组对象内有n-1个元素,编写代码向数组的任意位置插入一个新的元素值。元素值及索引位置由键盘输入
  2. 对于一个长度为n的数组对象,数组对象内有n个元素,编写代码移除任意位置的元素对象值。待移除元素的索引位置由键盘输入
  3. 有n个人一起玩游戏,每隔两个人需要有一个玩家大喊bingo。喊bingo的玩家将会被出局。编写代码输出玩家出局的顺序。直到最后一名玩家为止

10 包的创建与导入

10.1 包的作用

包是一种逻辑结构,包对应于一个物理路径。包用于在项目中管理代码。通过包可以有效避免类的命名冲突。在大型项目中通常将功能相似的额一组代码放在一个包内。包可以包含类或子包。

10.2 包命名规范

  • 域名倒序

www.ay.edu

edu.ay

  • 全部由小写字母组成
  • 组成结构:域名.项目名.模块名

10.3 Package关键字

10.3.1 作用

描述了当前类的所属包。

10.3.2 语法

java
Package 包名;

10.3.3 示例

java
package com.xja.work.work01;

10.3.4 注意事项

  • package语句必须在文件的第一行

10.4 import关键字

10.4.1 作用

导入(引入)一个包下的类(或所有类)

在Java中可能会出现同名类,因为类名相同,所以调用时会产生歧义。所以通过导包的方式来说明被调用类的具体位置。

10.4.2 语法

java
Import 包名.类名;
Import 包名.*;

10.4.3 示例

java
import java.util.Date;
//import java.sql.Date;
public class Work01 {
 public static void main(String[] args) {
 Date d = new Date();
 java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis());
 }
}

10.4.4 说明

  • import导入包时可以使用*(任意类)通配符
  • 使用同包下的其他类,则不需要导包
  • 相同的包在同一个文件中只需要导入一次

10.4.5 常用包(了解)

  • Java.lang:包含了Java语言的核心类,如:Math、String、Integer等
  • Java.io:包含了输入/输出功能的相关类
  • Java.net:包含了网络相关的接口和类
  • Java.util:包含了一些常用工具类
  • Java.sql:包含了Jdbc操作的相关类和接口
  • Java.text:包含了文本操作或格式化操作的相关类

11 方法

11.1 概述

方法就是实现了特定功能的一个代码段,通过方法名可以对方法进行重复调用。

11.2 定义语法

11.2.1 语法

java
修饰符 返回值类型 方法名([参数列表]){
 [方法体]
 [return 返回值;]
}

11.2.2 说明

  • 修饰符:用于描述方法的可见范围、方法的类型
  • 返回值类型:限定方法的返回结果的数据类型,如果方法没有返回值则用void进行标注
  • 方法名:自定义名成,命名规则与变量的命名规范(camel)相同
  • 参数列表:方法中使用到的数据如果需要由外部进行传入,则可以通过参数进行传入
  • 方法体:方法所需要完成的功能
  • Return:return语句通常用于跳出当前方法,通过return语句可以向方法的调用者返回数据

11.2.3 示例

java
/**
 * 定义无参、无返回值的方法
 */
 public static void method01() {
 System.out.println("欢迎使用学生管理平台");
 System.out.println("1、新增学生信息");
 System.out.println("2、查询学生信息");
 System.out.println("9、退出");
 }

 /**
 * 定义带参、无返回值的方法
 * 根据性别和名字输出一句话
 * 形参:定义方法时的参数称为形参
 */
 public static void method02(String name,boolean gender){
 System.out.println(name+(gender?"先生":"女士")+"您好");
 }

 /**
 * 定义无参、带返回值方法
 */
 public static int method03(){
 Random r = new Random();
 return r.nextInt(100);
 }

 /**
 * 带参、带返回值方法
 */
 public static int method04(int num1, int num2){
 return num1+num2;
 }
}

11.3 方法的调用

java
[数据类型 变量名 =] 方法名([参数列表])

调用方法时,注意观察方法需要什么(参数),可以给我们什么(返回值)。

11.4 调用过程

  • 代码首先从main()方法开始进行执行,当遇到其他方法时,将其他方法加载到栈中(入栈)
  • 然后方法的执行将离开当前方法,并对新方法开始进行执行
  • 新入栈的方法执行结束后返回到元方法的执行位置继续向下执行

11.5 方法的参数

11.5.1 参数类型

  • 形参:定义方法时的参数称为方法的形参,形参没有任何数据值
  • 实参:调用方法时传递参数称为实参,实参是有具体值的。

11.5.2 方法的参数传递

  • 基本类型参数:当方法的参数为基本类型参数时,方法内对形参的修改不会影响到实参,因为方法在传递时传递的是参数的副本(仅传递数据)
  • 引用类型参数:当方法的参数为引用类型参数时,方法内对形参的修改会影响到实参,因为参数传递过程中传递的是引用内存的地址(存储数据的内存空间被人操作了--老家被人抄了)

11.5.3 可空参数

  1. 示例
java
/**
 * 接收一个数组对象作为参数
 * `@param` params
 */
public static void method01(int[] params){
 if(params!=null) {
 for (int t : params) {
 System.out.println(t);
 }
 }
}

/**
 * 可空参数
 * 接收一组参数,也可以不传递任何参数
 * `@param` params
 */
public static void method02(int...params){
 if(params!=null) {
 for (int t : params) {
 System.out.println(t);
 }
 }
}
java
public static void main(String[] args) {
 int[] arr = {2,7};
 //调用方法
 method01(arr);
 //传递了参数,只不过参数的内容为空
 method01(new int[]{});
 method01(null);
 //报错,必须为方法提供一个参数
 //method01();
 //调用可空参数的方法
 //以数组对象作为参数
 method02(arr);
 //以数值序列为参数
 method02(8,34,12);
 //以null值作为参数
 method02(null);
 method02();
}
  1. 注意
  • 当方法的参数列表有多个参数时,可空参数必须在参数列表的最后

11.6 递归调用

11.6.1 概述

我们将方法自身进行调用的过程就称为递归调用。

11.6.2 需求

计算5!的阶乘。12345

11.6.3 实现

java
public class Test {
 public static void main(String[] args) {
 int i = 5;
 //System.out.println(method01(i));
 System.out.println(method02(i));
 }

 /**
 * 计算5的阶乘
 * `@param` i
 * `@return`
 */
 private static int method02(int i) {
 if(i == 0)
 return 1;
 else
 return i * method02(i - 1);
 }

 /**
 * 计算5的阶乘--循环方式进行实现
 */
 private static int method01(int num) {
 //保存计算结果
 int result = 1;
 for(int i = 1; i <= 5; i++){
 result *= i;
 }
 return result;
 }
}

11.6.4 注意

  • 设计递归方法时一定要求调用的结束条件,否则可能会造成堆栈溢出

12 类和对象

12.1 编程方式

  • 面向过程:面向过程关注的是每个功能的实现
  • 面向对象:面相对象关注的是一个整体。代码的编写是围绕着对象进行实现

12.2 类

12.2.1 概述

类是从一类事物中抽取出来的(抽取的是事物的公共属性和行为),类是对一类事物的抽象描述。类是一个模版。

Java中类是代码的最小组织单位。

12.2.2 定义语法

java
修饰符 class 类名{
 [类成员]
}

12.2.3 说明

  • 修饰符:用于说明类的可见性及类的类型。
  • Class:用于类声明的关键字
  • 类名:自定义名称,类命名规范:
  • 采用有意义的英文单词进行命名
  • 组成类名的各单词首字母大写
  • 类成员:类内部包含的成员
  • 成员字段(表示类所具有的属性)
  • 构造器(用于在实例化对象时初始化成员字段的值)
  • 成员方法(用于表示类所具有的行为)

12.2.4 示例

java
public class Student {
 //属性:学号、姓名、年龄、性别、所属年级
 public String stuNo;
 public String stuName;
 public boolean gender;
 public int age;
 public String grade;

 //行为:学习、自我介绍
 /**
 * 学习
 */
 public void study(){
 System.out.println(stuName+"正在教室努力学习.......");
 }

 /**
 * 自我介绍
 */
 public void show(){
 System.out.print("大家好,我叫" + stuName+",我今年" + age);
 System.out.print(",我是一个性格开朗的"+(gender?"男":"女")+"孩子,");
 System.out.println("我来自于" + grade);
 }
}

12.2.5 注意事项

  • 一个Java文件中可以包含多个类
  • 被public修饰的类必的名称须和文件名相同,且只能有1个

12.3 对象

12.3.1 概述

一类事物中的一个真实个体,对象是具体化的。

12.3.2 对象的创建与使用

  1. 创建对象
java
类型名 对象名 = new 构造器([参数列表])
  1. 说明
  • new:操作符,用于向堆内存申请空间
  • 构造器:用于在对象实例化过程中初始化成员字段的值
  1. 示例
java
//创建对象(对象的实例化)
Student stu1 = new Student();

12.3.3 对象的使用

  1. 语法
对象名.成员名
  1. 示例
java
public class Test {
 public static void main(String[] args) {
 //创建对象(对象的实例化)
 Student stu1 = new Student();

 //使用对象
 stu1.stuName = "陈超楠";
 stu1.gender = false;
 stu1.age = 19;

 stu1.study();
 stu1.show();
 }
}

面向对象编程:代码编写过程中围绕对象进行实现的过程

  1. 成员调用机制
  • 同类内:直接进行调用
  • 不同类调用:先实例化对象,然后通过对象名进行调用
  1. 对象的生命周期

生命周期:一个对象从创建到销毁的过程就称为生命周期

  • 代码执行结束

当一个方法执行结束后,方法内的所有实例化的对象都将称为垃圾。

  • 对象被赋值为null

当一个对象被赋值为null后,如果内存中的对象没有被其他对象引用,则该对象将被视为垃圾。

  • 一个对象被多次引用

当对象被多次引用后,只要还有引用,则内存中的那个对象就不是垃圾。

12.4 类的成员

12.4.1 成员字段

  1. 概述

定义在类内部的一段就称为成员字段,成员字段通常用于表示事物所具有的属性。

  1. 示例
java
public class Student {
 //属性:学号、姓名、年龄、性别、所属年级
 public String stuNo;
 public String stuName;
 public boolean gender;
 public int age;
 public String grade;
}
  1. 说明

成员字段可以被同类内所有方法所使用。

12.4.2 构造器(Constructor)

  1. 概述

构造器也称为构造函数,构造器是一种特殊的函数。构造器用于在实例化对象时初始化成员字段的值。

  1. 示例
java
public class User {
 public int no;
 public String name;
 public int age;

 //构造器
 public User(int no, String name, int age){
 this.no = no;
 this.name = name;
 this.age = age;
 }

 public void show(){
 System.out.println("工号:" + no);
 System.out.println("姓名:" + name);
 System.out.println("年龄:" + age);
 }
}
  1. 特点
  • 构造器的名字与类名相同
  • 构造器没有返回值类型
  • 构造器只能在实例化对象时通过new操作符进行调用
  • 类中如果没有显式提供构造器,则编译器将默认提供一个无参构造器。如果显式提供了构造器,则编译器将不提供任何构造器
  1. 快捷键
  • Alt+Insert ==》Constructor
  • 鼠标右键(菜单)==》Generate==》Constructor

12.5 练习

  1. 定义三角型类,类中提供计算三角形周长的方法

13 面向对象三大特性

13.1 面向对象三大特性

  • 封装
  • 继承
  • 多态

13.2 封装

13.2.1 概述

封装的主要目的就是隐藏,隐藏类内部的实现细节。面向对象的核心设计思想是:高内聚(类内部的事情由类自身来完成—自身功能很强大),低耦合(开放很少量的公共接口用于用户使用)

13.2.2 访问修饰符

当前类同包类不同包子类同项目
private×××
默认(Friendly)××
protected×
public

说明:通过使用不同修饰符可以对类和类成员的可访问范围进行限制。

  • private:私有的,只能用于修饰类成员,被private修饰符修饰的成员只能在当前类中被访问
  • friendly:默认的,也称为友元的(package private—包私有)。如果一个类(或成员)没有使用任何访问修饰符,则说明该类(或成员)具有默认的访问修饰符。使用默认访问修饰符的成员只能被同一个包中的其他类所访问。无法被其他包中的类所使用。这种特性就称为包访问性。
  • protected:保护的,该修饰符只能修饰类成员,被protected修饰的成员可以被当前类、同包类或其他包中的子类所访问
  • public:公共的,该修饰符可以修饰类及类的成员,被public修饰符修饰成员没有访问限制

13.2.3 信息的隐藏

  1. 实现方式

通过私有的访问修饰符对字段进行修饰,避免外界直接进行访问。

  1. 示例
java
public class Student {
 //属性(成员字段)
 //通过private修饰符对字段进行隐藏(目的:保护数据,避免数据被直接操作)
 private String stuNo;
 private String name;
 private int age;
}

| :--- |

  1. 问题

成员字段私有化后无法直接进值的读写操作。

  1. 解决
  • 使用构造函数初始化成员字段的值
  • 只能在实例化对象时对字段的值进行初始化
  • 无法修改或读取字段的值
  • 使用get/set访问器读写字段的值
  1. get/set访问
  • 本质

get/set访问器就是按照特定命名规则进行命名的方法。

  • 快捷键
  • Alt+insert ==》Getter and Setter
  • 作用
  • Get访问器:用于获取字段的值
  • Set访问器:用于修改字段的值
  • 使用
  • 只读属性:只提供了get访问器
  • 只写属性:只提供set访问器
  • 可读写访问器:同时提供get、set访问器
  • 示例
java
public class Student {
 //属性(成员字段)
 //通过private修饰符对字段进行隐藏(目的:保护数据,避免数据被直接操作)
 //自读属性
 private String stuNo;
 //可读写属性
 private String name;
 private int age;

 //构造器初始化成员字段的值
 public Student(String stuNo, String name, int age) {
 this.stuNo = stuNo;
 this.name = name;
 this.age = age;
 }

 //定义方法读取字段的值
 //用于读取数据的值
 public String getStuNo() {
 return stuNo;
 }

 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 || age > 200)
 this.age = 0;
 else
 this.age = age;
 }
}
java
public class Test {
 public static void main(String[] args) {
 Student s1 = new Student("2005001","Tom",22);

 //使用字段的值
 //修改字段值
 //通过set访问器修改字段的值
 s1.setName("");
 s1.setAge(-800);

 //通过get访问器读取字段的值
 System.out.println(s1.getStuNo());
 System.out.println(s1.getName());
 System.out.println(s1.getAge());
 }
}
  • 意义
  • 可以在读写字段时设置规则,避免数据值被随意修改造成歧义
  1. 成员变量与局部变量
  • 变量的定义
  • 成员变量:定义在类中的变量就称为成员变量,也称为成员字段。成员变量可以被当前类的所有成员方法所使用
  • 局部变量:定义在方法内的变量就称为局部变量(包含方法的参数、方法内定义的变量)。局部变量只能在所定义的方法内进行使用
  • 示例
java
public class VarDemo {
 //成员变量
 private String name = "我是成员变量";

 //定义方法
 public void method01(String str){
 System.out.println("str=" + str);
 System.out.println("name=" + name);
 }

 public void method02(){
 //无法访问变量str。因为str是局部变量。他定义在method01中
 //System.out.println("str=" + str);
 System.out.println("name=" + name);
 }
}
  • 调用
  • 就近原则(当成员变量与局部变量同名时,方法内部优先调用局部变量)

方法执行时首先在方法内对变量进行查找,如果找到则直接进行使用,否则继续向更大范围(对象本身)进行查找。如果查找失败则产生异常。

  • 问题

当全局变量与方法的局部变量同名时,方法执行过程中会优先调用局部变量。如何在方法内调用成员变量?

  • 解决

可以在方法内部通过this关键字对成员字段进行调用。

  • This关键字

This关键字就是当前类型的对象。this关键字通常用于在成员变量和局部变量同名时调用成员变量。

13.2.4 方法的隐藏

  1. 概述

通过适当的访问修饰符对方法进行修饰,然后通过公共的访问接口供用于进行访问。

  1. 示例
public class Student { _//成员字段 __ _private String no; private String name; private int age; _//构造器 __ _public Student() { _//调用当前类的其他构造函数 __ _this("",0); _//no = generate(); __ _} public Student(String name, int age) { this.name = name; this.age = age; _//系统自动生成学号 __ _no = generate(); } …… _/** __ * 生成学生的学号 __ * @return __ */ __ _private String generate(){ Random r = new Random(); return "202501" +String.format("%05d",r.nextInt(100)); }}

13.2.5 封装的设计思想

隐藏内部复杂的实现细节,提供公共的访问接口用于调用(暴露该暴露的,隐藏该隐藏)

13.2.6 方法重载

  1. 概述

在同一个类中,如果出现了1个以上的同名方法,这些方法就构成了重载。

  1. 要求
  • 重载方法在同一个类中
  • 具有不同的参数列表(看方法的签名是否一致)
  • 返回值不做参考
  1. 示例
java
public class Cal {
 /**
 * 两个整数相加的计算方法
 */
 public int add(int num1, int num2){
 return num1+num2;
 }

 /**
 * 两个浮点数相加的计算方法
 */
 public float add(float num1, float num2){
 return num1+num2;
 }
}
  1. 意义
  • 方法重载可以提高程序代码调用的灵活性,满足不同用户的需求
  • 当传递参数类型发生改变时不需要修改代码,提高程序的健壮性
  1. 注意事项
  • 同类型的可空参数和数组无法构成重载

13.3 继承

13.3.1 需求

  • 学生类:学号、姓名、性别、年龄、所属年级、学习
  • 员工类:工号、姓名、性别、年龄、所属部门、工作

13.3.2 为什么需要继承

当多个类中有相同的属性和行为时,将公共的属性和行为抽取到一个独立的类中。然后通过继承的方式可以让代码变得更加加单清晰。提高代码的复用性。

13.3.3 名词

  • 父类:被继承的类称为父类,也称为基类或超类。父类中保存类所有子类的公共属性和行为。
  • 子类:继承了其他类的类就是子类,子类也称为派生类。

注意:子类和父类之间需要符合“is a”的关系,子类 is a 父类。如:学生(类)和人(类)

13.3.4 语法

java
[修饰符] class 子类名 extends 父类名{
 类成员
}

13.3.5 示例

java
【父类】
public class Person {
 private String name;
 private int age;
 private boolean gender;

 //构造器
 public Person() {
 }

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

 //get/set访问器
 public String getName() {
 return name;
 }

 public void setName(String name) {
 this.name = name;
 }
 ……
}
java
【子类】
public class Student extends Person {
 private String stuNo;
 private String grade;

 //构造器
 public Student() {
 }

 public Student(String stuNo, String grade) {
 this.stuNo = stuNo;
 this.grade = grade;
 }

 public Student(String name, int age, boolean gender, String stuNo, String grade) {
 super(name, age, gender);
 this.stuNo = stuNo;
 this.grade = grade;
 }

 //get/set访问器
 ……

 //自定义方法
 public void study(){
 System.out.println(this.stuNo);
 System.out.println(this.getName());
 }
}

13.3.6 作用

  • 继承可以减少冗余代码,提高代码的复用性
  • 继承可以有利于代码的扩展(子类可以对父类进行扩展)
  • 继承实现了类和类之间的关系,为多态提供了基础

13.3.7 说明

  • 继承关系中子类将拥有父类的所有成员,但私有成员对于子类是不可见的
  • 不能仅需要使用其他类的某一部分功能而去继承
  • 在子类中,可以对父类进行扩展,子类可以拥有自己的属性和行为
  • 在Java中,继承的关键字是”extends”,子类不是父类的子集,而是对父类的扩展。
  • 默认情况下,所有类的默认父类为Object类

13.3.8 继承的规则

  1. 子类不能直接访问父类的私有成员,如果是字段,则可以通过开放的接口(get/set访问器)进行访问
  2. Jav中只有单一继承,不允许多继承
  • 一个子类只能有一个父类,但一个父类可以有多个子类

13.3.9 方法重写(Overrides)

  1. 概述

在继承关系中,子类将继承父类的所有方法,当父类中的方法无法满足子类的要求时,java允许在子类中对父类中的方法进行重新定义,这种重新定义的行为就称为方法的重写(Overrides)。

所谓方法重写就是在子类中对父类中已经存在的同名方法进行实现。重写后方法在功能上一定与父类的原有方法所有区别。所以方法的重写也称为覆盖或重置。

  1. 示例
java
【父类】
public class Printer {
 /**
 * 提示信息
 */
 public void tip(){
 System.out.println("请按下打印按钮");
 }
}
java
【子类】
/**
 * 重写方法
 * 快捷键:
 * Ctrl + o
 * Alt+Insert==>Override Methods...
 */
public class EngPrinter extends Printer {
 //对父类中已经存在的同名方法再次进行实现
 //对父类方法进行重写
 //public void tip(){
 // System.out.println("Plrease Press 'Print' Button");
 //}
 `@Override`
 public void tip() {
 super.tip();
 }
}
  1. 快捷键
java
Ctrl + o
Alt +Insert ==》Override Methods

| --------------------------------------------- |

  1. 要求
  • 方法必须存在于父子类中
  • 子类重写父类方法时,要求必须和父类的方法具有相同的名称、参数列表及返回值
  • 重写过程中,子类方法的访问修饰符不能小于父类中(被重写)方法的修饰符
  1. 注意事项
  • 重写过程中,重写方法不能是静态方法(static)
  1. 【问】:简述重写与重载的区别
  • 重写方法存在于父子类中,方法间属于垂直关系;重载方法存在于同一个类中,方法间属于水平关系
  • 重写方法要求必须具有相同的名称、返回值类型和参数列表;重载方法要求方法具有相同的名称,且参数列表一定不同
  • 重写方法要求子类中重写修饰符必须不能小于父类中被重写方法的修饰符;重载没有修饰符的要求
  • 重写要求子类中的返回值类型必须和父类保持一致;重载没有返回值类型的要求

13.3.10 Super关键字

  1. 作用

当父类与子类同时存在同名成员时,通过super关键字可以用来调用父类中的成员(在引用关系中super表示父类对象的引用)。

  • 在继承关系中,通过super关键字调用父类中的属性
  • 在继承关系中,通过super可以调用父类中定义的构造函数
  • 在继承关系中,通过super可以调用父类的方法
  1. 示例
java
【父类】
public class Parent {
 //字段
 protected String str = "parent.str";

 //构造器
 public Parent() {
 }

 public Parent(String str) {
 this.str = str;
 }

 //方法
 protected void show(){
 System.out.println("父类中的show()方法被调用了。");
 }
}
java
【子类】
public class Child extends Parent {
 //定义了同名字段
 public String str = "Child.str";

 //默认调用了父类的无参构造
 public Child() {
 }

 //调用父类的带参构造函数
 public Child(String str, String str1) {
 //通过super进行调用,super()表示父类的构造函数
 //super调用父类构造函数时必须是代码段的第一行
 super(str);
 this.str = str1;
 }

 //重写了父类中的show()方法
 protected void show(){
 System.out.println("子类中的show()方法被调用了。");
 }

 //当父类和子类同时存在同名成员时,优先调用子类成员
 //如果需要调用父类成员该如何实现?
 public void print(){
 //调用成员时依然准遵守就近原则,所以子类默认情况下子类成员被优先调用
 //通过super关键字调用父类的字段
 System.out.println("str=" + super.str);
 super.show();
 }
}
  1. 注意
  • 当子类和父类出现同名成员时,可以通过super关键字对父类成员进行调用
  • 在子类的构造函数中使用super调用父类构造函数时,调用语句必须是代码段的第一行
  • super和shis比较相似,this代表当前类型对象的引用,super表示的是父类对象的引用
  1. 调用父类构造函数

在继承关系中,子类是无法继承父类的构造函数。

  • 子类所有构造函数默认调用的是父类的无参构造函数
  • 当父类中声明无参构造函数时,子类的构造函数同需要通过super或this来指定当前类或父类中相应的构造函数。在使用过程只能使用其中一种方式。且必须是代码的第一行
  • 如果子类构造函数中没有显式调用父类或当前类的构造函数,且父类中没有定义无参构造函数,则编译器将会产生错误
  1. this和super
  • this默认访问当前对象的成员;super则默认访问父类的成员
  • this表示当前类的构造函数;super则表示父类对象的构造函数

13.4 多态

13.4.1 概述

所谓多态是指同一个对象的同名方法在不同环境下呈现出不同的行为就称为多态。

在Java中,多态是面向对象程序设计的特性之一,也是面向对象中的重要概念。

13.4.2 要求

  • 父类的引用指向了子类对象
  • 可以直接应用与抽象类和接口进行实现
  • 子类对父类中的方进行重写

13.4.3 分类

  • 运行时多态:实际运行过程中赋值的对象决定
  • 编译时多态:声明的对象的类型决定

13.4.4 特点

在多态中,凡是父类出现的位置都可以用子类对象进行替换。

  • 一个变量只能有一种数据类型
  • 一个引用可以指向(引用多种类型)不同类型的对象

13.4.5 示例

java
【父类】
public class Tv {
 //字段
 private String brand;

 //构造器
 public Tv(String brand) {
 this.brand = brand;
 }

 //自定义方法
 public void showWelcome(){
 System.out.println();
 }
}
java
【子类1】
public class Hesine extends Tv {
 //构造器
 public Hesine(String brand) {
 super(brand);
 }

 `@Override`
 public void showWelcome() {
 System.out.println("海信连通世界......");
 }
}

| 【子类2】
略 |

java
【测试类】
public class Test {
 public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);

 //使用多态:父类对象指向了子类引用
 //编译时多态
 Tv tv1 = new Hesine("海信");
 tv1.showWelcome();

 //运行时多态
 //运行时多态
 Tv tv2;
 System.out.print("品牌(1、海信;2、LG):");
 int choose = sc.nextInt();

 if(choose==1)
 tv2 = new Hesine("海信");
 else
 tv2 = new Lg("LG");

 tv2.showWelcome();
 }
}

13.4.6 转型(类型转换)

  1. 向上转型

将一个父类对象的额引用指向一个子类对象就称为向上转型(upcast ing)。向上转型是自动(隐式)完成的。

  1. 向下转型

将一个父类对象强转为子类对象就称为向下转型。向下转型是强制完成。

13.4.7 示例

【父类】
public class Person { private String name; private int age; private boolean gender; _//构造器 __ _public Person() { } public Person(String name, int age, boolean gender) { this.name = name; this.age = age; this.gender = gender; } …… public void say(){ System.out.println("大家好,我叫" + name+",我今年" + age+"岁,我是一个性格开朗的" + (gender?"男孩":"女孩")); } }
【子类1】
public class Student extends Person{ _//构造器 __ _public Student() { } public Student(String name, int age, boolean gender) { super(name, age, gender); } }
【子类2】
public class Worker extends Person{ _//构造器 __ _public Worker() { } public Worker(String name, int age, boolean gender) { super(name, age, gender); } _//自定义方法 __ _public void work(){ System.out.println(this.getName()+"正在办公室努力工作......."); } }
【测试类】
public class Test { public static void main(String[] args) { _//Person p =new Student("Tom",20,true); __ //p.say(); __ //将一个子类对象传递给call方法,方法中的参数是父类型的(向上转型) __ //call(new Student("Tom",20,true)); __ //向上转型 __ _call(new Worker("张三",43,true)); } _/** __ * 调用对象的方法 __ * @param __p __ __*/ __ _private static void call(Person p){ p.say(); _//无法调用work()方法 __ //子类直到父类有什么,但父类不知道子类扩展了什么 __ //instanceof:判断对象是否属于某一个类型 __ _if(p instanceof Worker){ _//向下转型()执行强转操作} __ _Worker worker = (Worker) p; worker.work(); } } }

13.4.8 Instanceof操作符

  1. 作用

检测对象是否属于某一个类型。

  1. 语法
java
对象 instanceof 类型名

13.5 总结

13.5.1 面向对象特性

  1. 封装

隐藏内部实现细节,只提供公共接口(不要去关注与你无关的内容)

  1. 继承

继承的目的是提高代码的复用性,减少冗余代码

  1. 多态

提高代码的灵活性,提高代码的通用性。通常也称为接口重用或接口服用。多态的要求:

  • 具有继承关系
  • 子类重写了父类的方法
  • 父类引用指向子类对象

13.6 作业

  1. 开发银行储户管理系统。基本需求如下:
  • 系统运行后需要操作员进行登录
  • 默认账号:0661,密码:123456
  • 登录失败后显示账号已被冻结,登录成功后显示系统菜单并接收用户的选择
  1. 开户

  2. 存款

  3. 取款

  4. 余额查询

  5. 退出

     暂时只支持中文
    
  • 实现储户开户功能,数据包含:卡号、姓名、余额、状态
  • 实现存款操作
  • 实现取款操作
  • 实现余额查询功能
  • 实现退出功能
  1. 使用面向对象的方式实现计算器,要求支持+、-、*、/运算
  2. 编写代码模拟电视机和遥控器之间的关系。电视机可以有多个不同的品牌,通过遥控器可以控制不同品牌电视机的开机与关机操作。
  3. 编写代码实现人机猜拳游戏。基本要求如下:
  • 玩家可以指定每局游戏的回合数
  • 每回合结束后输出游戏结果
  • 游戏结束后输出玩家得分
  1. 编写代码实现对战游戏功能。基本要求如下:
  • 游戏双方都有不同的攻击力和防御力
  • 双方交替攻击,被攻击方会收到一定的伤害,同时防御力可以抵消一定的伤害
  • 不同类型的英雄的攻击方式是不同的(法师--法术攻击;武士--物理攻击)
  • 游戏双方中一方死亡则游戏结束

14 Java的内存模型

14.1 Cpu的多级缓存模型

14.1.1 Java的内存模型

  • 打破不同指令集之间不兼容问题
  • 解决多线程的安全性问题

14.1.2 Cpu的缓存

运算过程中,Cpu到内存中读取数据,Cpu的执行速度相对较快。内存的读写速度相对较慢,内存将严重影响Cpu的执行速度。

为了解决这个问题,Cpu中加入了高速缓存。每次运算过程中Cpu首先将数据读取到缓存中,然后在运算过程中将数据放到缓存中进行临时存储。运算完成后将数据写回到内存。

14.1.3 多线程下的数据安全性问题

目前Cpu都是多核的,这意味着有多个Cpu。每个Cpu中又有一个缓存,也就相当于有多个缓存。

数据在内存中只有1分,多线程中数据被夹在到多个缓存中,线程间对缓存数据的修改是不会影响到其他缓存的。这也意味着缓存间数据是不同步的,这将造成线程间数据安全性问题。

14.2 Java的内存模型

14.2.1 内存的划分

为了解决数据安全性问题,Java的内存模型主体分为:主内存和工作内存。

  • 主内存由主线程进行使用,内部存储共享变量和数据
  • 每个线程创建时都会创建一个工作内存,子线程创建的数据都在工作内存中存储
  • 当其他线程需要使用主内存数据的时候,他会先到主内存中拷贝一个数据的副本到工作内存。运行过程中线程操作工作内存中的数据副本。运算结束后将数据写回到主内存
  • 线程间数据相互是独立的

14.2.2 8个基本指令

  • Lock:锁,当线程从主内存读取数据时,Jvm会将主内存中被读取变量锁定
  • read:读,读取主内存中的数据,数据读取后通过load指令将数据传输到工作内存
  • load:读取主内存中的数据,然后装载到工作内存中
  • use:数据被装载到工作内存后可以参与各类运算,通过use指令使用数据
  • assign:运算结束后将数据写回到工作内存,assign的作用就是赋值
  • store:运算完成后将工作内存中的数据写回到主内存。通过store完成存储操作
  • wirete:数据存储到主内存还不行,还需要将数据写回到变量中。wirete指令将数据写回到变量
  • unlock:数据在主线程中一直处于锁定状态,使用结束后需要使用unlock进行解锁

14.3 Java的跨平台实现

14.3.1 Java的扩平台实现

通过不同平台的虚拟机从软件层面上进行实现。

14.3.2 代码执行过程

执行Java程序时,代码被Java的类加载子系统加载到内存中,然后通过字节码执行引擎对代码进行执行。

14.3.3 内存空间的组成

存放引用类型对象,凡是new出来的对象都存放在堆中,但是也会有例外,偶尔也会有存储在栈中的(标量替换)。

官方的说法就是Java Virtual Machine Stacks,称为虚拟机栈。我们可以称为线程栈,栈中存储的是局部变量。

  1. 程序计数器

程序执行过程中可能需要去执行其他区的方法,其他方法执行结束后会重新回到当前程序,当前程序从哪里开始进行执行?

在Jvm中使用程序计数器存储执行位置。所以程序计数器也是一块内存空间,在计数器的内部记录了程序执行到的位置。

  1. 方法区

Jdk1.8之前称为方法区,方法区将内存分为:新生代、中生代和老年代。Jdk1.8后改为元空间。方法区对应于物理内存。

方法区中存储常量、字符串常量池、静态变量、类信息(元数据)。这些数据都被所有对象所共享。

  1. 本地方法栈

在Java代码中有native的,所有native代码都在本地方法栈中进行执行。

14.4 static关键字

14.4.1 为什么用static关键字

类中的指定字段的值对所有对象是共享的,该如何进行实现?

通过static关键字声明的变量就可以被所有对象所共享。因为static关键字声明的变量属于。在内存中只有1分,所以也称为类变量。

  • 类变量可以被该类型的所有对象所共享
  • 在设计的时候,分析哪些属性不应该是对象的专属属性,就将这些属性设置为类属性(如果是方法就是类方法)
  • 如果方法的调用与调用者无关,则这些方法同样可以声明为类方法

14.4.2 static的使用范围

  • 属性
  • 方法
  • 代码块
  • 内部类

14.4.3 静态字段

  1. 概述

被static关键字修饰的字段称为静态字段。

  1. 示例
java
/**
 * 静态成员
 */
public class Student {
 //学号(实例成员)
 public int stuNo = 1000;
 //学号2(静态成员)
 public static int stuNo2 = 2000;
}
java
【测试】
public static void main(String[] args) {
 //通过类型名可以直接访问stuNo2(静态字段)
 System.out.println(Student.stuNo2);

 //实例化对象
 Student s1 = new Student();
 Student s2 = new Student();

 //访问对象的成员
 //访问对象的实例字段
 System.out.println(s1.stuNo);
 //访问对象的静态字段
 System.out.println(s1.stuNo2);

 //实例字段属于对象,在对象中对实例字段值的修改不会影响到其他对象
 s1.stuNo = 8990;
 System.out.println(s2.stuNo);

 System.out.println(s2.stuNo2);
 //静态字段属于类,被所有对象所共享,任意对象对静态字段值的修改都会影响到其他对象
 s1.stuNo2 = 9000;
 System.out.println(s2.stuNo2);
}
  1. 分析

  2. 说明

  • 静态字段随类的加载而加载,静态字段被当前类型所有对象所共享
  • 静态字段属于类,所以可以通过类名进行调用

14.4.4 静态方法

  1. 概述

被static关键字修饰的方法称为静态方法,静态方法可以通过类名直接进行调用。

  1. 示例
java
class Student{
 private String str1 = "non static";
 private static String str2 = "static";
 /**
 * 实例方法
 * 实例方法中可以使用静态字段,也可以调用实例字段
 */
 public void show1(){
 System.out.println(str1);
 System.out.println(str2);
 System.out.println("Student.show() is do.......");
 }

 /**
 * 静态方法
 * 静态方法中只能使用静态字段
 */
 public static void show2(){
 System.out.println(str2);
 System.out.println("Student.show2() is do.......");
 }
}
java
public static void main(String[] args) {
 //静态方法可以通过类名进行直接调用
 Student.show2();

 //实例方法
 Student stu = new Student();
 //调用实例方法
 stu.show1();
 //调用静态方法
 stu.show2();
}
  1. 说明
  • 实例方法中可以直接使用静态字段和实例字段
  • 静态方法中只能直接使用静态字段
  1. 注意

在静态方法中不能使用this关键字。

14.4.5 代码块

  1. 概述

代码块用于在对象初始化之前进行成员的初始化操作。

  1. 语法
java
{
 初始化代码;
}
  1. 示例
java
/**
 * 代码块示例
 */
class Student{
 private int age;
 private String name;
 private static String grade;

 /**
 * 代码段
 * 在构造器之前被调用,用于实例化对象前对对象的字段进行初始化操作
 */
 {
 this.age = 18;
 this.name = "无名";
 grade = "一年1班";
 }
 ……
}
java
public static void main(String[] args) {
 Student stu = new Student();
 System.out.println(stu.getName() + " ====== " + stu.getAge() + "======" + stu.getGrade());
}
  1. 注意
  • 代码块不能使用修饰符,如果使用修饰符只能使用static修饰符
  • 每次实例化对象时代码块都会被调用

14.4.6 静态代码块

  1. 概述

被static关键字修饰的代码块就称为静态代码块。

  1. 示例
java
class Student{
 private String str1;
 private static String str2;

 /**
 * 静态代码块
 * 在类加载后被调用,且只能被执行1次
 * 静态代码块用于对静态字段进行初始化
 */
 static {
 str2 = "def";
 System.out.println("静态代码块被执行了.......");
 }

 public void show(){
 System.out.println(str1 + " ====== " + str2);
 }
}
java
/**
 * 静态代码块
 */
public class Test {
 public static void main(String[] args) {
 Student stu = new Student();
 stu.show();

 Student s2 = new Student();
 }
}
  1. 代码块与静态代码块
  • 静态代码块只能被执行1次;代码块每次实例化对象时都会被执行
  • 静态代码块主要用于初始化静态字段;代码块级可以初始化静态字段,也可以初始化实例字段
  1. 说明
  • 静态代码块和实例代码块可以同时存在,静态代码块优先被执行
  • 可以同时存在多个代码块,执行顺序按照定义顺序一次进行执行
  1. 执行顺序

14.5 Final关键字

14.5.1 概述

Final表示最终的。

14.5.2 修饰对象

  • 方法
  • 成员字段
  • 方法参数

14.5.3 修饰类

  1. 示例
java
/**
 * final修饰类
 */
public final class Parent {
}

/**
 * 当Parent类使用final进行修饰时,产生错误
 * Cannot inherit from final 'com. xja. finale. demo01.Parent
 */
class Child extends Parent{}
  1. 特点
  • 被final修饰的类无法被继承
  1. 使用场景
  • 一个类为最终版本,不希望被继承时可以使用final关键字进行修饰(版本保护)。

14.5.4 Final修饰字段

  1. 示例
java
/**
 * final修饰字段
 * final修饰的字段必须进行初始化,否则将产生异常:
 * Variable 'name' might not have been initialized
 * 初始化方式:
 * a、声明时初始化
 * b、通过代码块进行初始化
 * c、通过构造器进行初始化
 * 特点:
 * final修饰的字段其值无法修改
 */
public class Parent {
 //声明时进行初始化
 private final String name = "abc";
 //通过代码块进行初始化
 private final String name2;
 //通过构造器进行初始化
 private final String name3;
 //无法通过方法进行初始化
 //private final String name4;

 {
 name2 = "abc";
 }

 public Parent() {
 name3 = "abc";
 }

 public void change(){
 //name4="abc";
 //final修饰的字段值无法修改
 //name3="ddd";
 }
}
  1. 特点

被final修饰的字段其值无法再次被修改,所以final修饰的字段也称为常量。常量的名称要求全部使用大写字母组成。

  1. 常量的初始化
  • 声明时进行初始化
  • 通过代码块进行初始化
  • 通过构造器进行初始化
  1. 说明
  • 基本类型常量其值无法被再次修改
  • 引用类型无法修改变量的引用地址,但可以修改变量引用对象的成员字段

14.5.5 Final修饰方法

  1. 示例
java
/**
 * final修饰方法
 */
public class Parent {
 public final void show(){}
}

class Child extends Parent{
 //无法重写父类的方法,因为方法是被final修饰的
 //public final void show(){}
}
  1. 特点
  • Final修饰的方法无法被子类进行重写。

14.5.6 Final修饰方法的参数

  1. 示例
java
/**
 * final修饰方法的参数
 */
public class Parent {
 public void print(int num1, final int num2){
 num1 = 20;
 //Cannot assign a value to final variable 'num2'
 //num2 = 77;
 }
}
  1. 特点

Final关键字修饰的方法参数无法被再次修改,只能使用方法参数的值。

【问】:final关键字的使用方式有哪些?

14.6 练习

  1. 编写一个类,类中有一个名为show()的方法。要求在测试类中无论创建多少个对象,只能有一个实例。并进行测试。
  2. 使用面向对象的设计思想实现计算器,要求支持+、-、*、/。

15 抽象类和接口

15.1 抽象类

15.1.1 概述

在面向对象中有些无法被具体化的概念,就可以使用抽象类进行描述。抽象类体现了数据抽象的思想,他是实现多态的一种机制。抽象类用来抽取子类的通用特征。抽象类作为子类的模版,他不能被实例化,只能作为父类进行使用。

15.1.2 语法

java
[修饰符] abstract class 类名{
 [类成员]
}

15.1.3 示例

java
/**
 * 抽象类
 * 抽象类被abstract关键字修饰,抽象类中可以包含字段、构造器及方法
 */
public abstract class Parent {
 //私有成员字段
 private int age;
 //静态字段
 private static String name;
 //常量
 private final float blance = 0.0f;

 //构造器
 public Parent() {
 }

 //自定义方法
 public void show(){}
}

//继承了抽象类
class Child extends Parent{}
java
/**
 * 测试抽象类
 */
public class Test {
 public static void main(String[] args) {
 //异常,抽象类不能被实例化
 //Parent p = new Parent();
 //实例化只能实例化子类对象
 Parent p = new Child();
 }
}

15.1.4 特点

  • 抽象类中可以包含构造函数,但不能用来实例化对象
  • 抽象类中可以包含抽象方法,而非抽象类中不能包含抽象方法

15.1.5 注意事项

  • 抽象类不能被实例化,实例化的工作必须由他的非抽象子类来完成
  • abstract不能与final一起修饰类
  • abstract不能与private、static、final及native一起连用修饰方法

15.2 抽象方法

15.2.1 概述

被abstract关键字修饰的方法就称为抽象方法。

15.2.2 作用

为子类提供一个强制性的方法模版。

15.2.3 语法

java
[修饰符] abstract 返回值类型 方法名([参数列表]);

15.2.4 要求

  • 抽象方法必须定义在抽象类中
  • 抽象方法没有方法体
  • 抽象方法必须在非抽象子类中进行实现

15.2.5 示例

java
/**
 * 父类
 * 父类中包含了抽象方法
 */
public abstract class Parent {
 public abstract void show();
}

//抽象类可以不对父类中的抽象方法进行实现
abstract class Clazz extends Parent{
}

/**
 * 非抽象子类必须对父类中的抽象方法进行实现
 * 如果在Clazz中已经对抽象方法进行了实现,则在Child中可以不进行实现
 */
class Child extends Clazz{
 `@Override`
 public void show() {
 }
}

15.3 接口

15.3.1 概述

接口是引用类型的,接口是抽象方法的集合。

  • Jdk1.7之前:只有抽象方法和常量
  • Jdk1.7之后:新增了默认方法和静态方法
  • Jdk1.9:支持私有方法

接口是一种公共的规范标准,只要符合规范标准,就可以实现通用。接口与抽象类相似,只是定义的形式存在差异。抽象类用来捕获子类的通用特性,接口是抽象方法的集合。

从设计角度来说,抽象类是对类的抽象,是一种模版的设计。接口是行为抽象,是一种行为规范。

15.3.2 定义语法

java
Interface 接口名{
 [接口成员;]
}

15.3.3 示例

java
【接口】
/**
 * 接口
 */
public interface Phone {
 /**
 * 定义抽象方法
 * 接口中的方法都是抽象方法,所以不需要显式使用public、abstract关键字进行修饰
 */
 public abstract void sendMessage();

 /**
 * 定义抽象方法
 */
 void call();
}
java
【实现类】
/**
 * 接口的实现类必须对接口中方法进行实现
 */
public class RedMi implements Phone {
 `@Override`
 public void sendMessage() {
 System.out.println("Send Message is do.......");
 }

 `@Override`
 public void call() {
 System.out.println("Call is do......");
 }
}
java
【测试类】
/**
 * 测试抽象方法
 */
public class Test {
 public static void main(String[] args) {
 Phone p = new RedMi();
 p.sendMessage();
 }
}

15.3.4 接口成员

  1. Jdk1.8-
  • 常量
  • 抽象类
  1. Jdk1.8
  • 默认方法
  • 静态方法
  1. Jdk1.9
  • 私有方法
java
/**
 * 接口成员
 * Jdk1.8-
 * 1、常量:接口中的字段默认使用public static final进行修饰
 * 2、抽象方法:接口中的抽象方法默认使用public abstract进行修饰
 * Jdk1.8
 * 3、默认方法:Jdk1.8中新增了默认方法,默认方法需要使用default关键字进行修饰,
 * 默认方法有方法体
 * 4、静态方法:Jdk1.8中新增了静态方法,静态方法可以使用public或默认修饰符进行修饰
 * (默认实际上使用的还是public修饰符)
 * Jdk1.9
 * 5、私有方法:Jdk1.9开始支持所有方法
 */
public interface ParentInterface {
 //接口成员
 //Jdk1.8-
 //字段--接口中的字段默认使用public static final进行修饰
 //public static final java.lang.String VERSION;
 String VERSION = "1.7.06";

 //抽象方法--接口中的抽象方法默认使用public abstract进行修饰
 void say();

 //Jdk1.8
 //默认方法--默认方法可以有方法体
 default int getNum() {
 Random r = new Random();
 return r.nextInt(100);
 }

 //静态方法:只能使用public或默认修饰符进行修饰
 static void show() {
 System.out.println("版本:" + VERSION);
 }

 //Jdk1.9
 //Jdk1.9中允许使用私有方法
 private void vlid() {}
}

15.3.5 接口与继承

java
【接口1】
/**
 * 接口1
 */
public interface Parent1 {
}
java
【接口2】
/**
 * 接口2
 */
public interface Parent2 {
}
java
【接口3】
/**
 * 接口3
 * 接口继承的关键字是extends
 * 接口与继承--接口允许多继承
 * 接口Child同时继承了接口2和接口3
 */
public interface Child extends Parent1, Parent2 {
}
  • 类只允许单一继承,接口允许多继承

15.3.6 接口与实现

java
【接口1】
/**
 * 接口1
 */
public interface Parent1 {
}
java
【接口2】
/**
 * 接口2
 */
public interface Parent2 {
}
java
【实现类】
/**
 * 类
 * 接口允许多实现。
 * 类实现接口使用implements关键字
 */
public class Child implements Parent1, Parent2 {
}
  • 一个类可以同时实现多个接口

15.3.7 多继承的意义

java
【接口1】
/**
 * 飞行能力
 */
public interface Fly {
 void fly();
}
java
【接口2】
/**
 * 游泳能力
 */
public interface Swim {
 void swim();
}
java
【实现类1】
/**
 * 凯
 */
public class Kai implements Run {
 `@Override`
 public void run() {
 }
}
java
【实现类2】
/**
 * 蓝
 */
public class Lan implements Swim, Run {
 `@Override`
 public void run() {
 }

 `@Override`
 public void swim() {
 }
}

接口的多继承(实现)可以让子接口(实现类)同时将多种不同的能力组合在一起进行使用,可以使得代码更加灵活强大(实现随意组合)。

15.3.8 说明

  • 接口的本质是契约、规范、标准
  • 接口就是规范,定义的是一组规范。体现了现实世界中的“如果你….则你必须能….”的思想

15.3.9 注意事项

  • 接口中不能定义构造函数
  • Java开发中,接口需要通过子类进行实现的方式进行使用
  • 如果类覆盖(实现)了接口的所有抽象方法,则实现类可以进行实例化
  • 如果实现类没有覆盖接口中所有抽象方法,则此类仍然可以是抽象类
  • 类可以实现多接口,这弥补了类的单一继承的限制
  • 接口之间支持多继承
  • 接口的使用体现了多态性,实际上可以看做是一种规范

15.3.10 应用场景

  • 如果拥有一些方法且希望他们有一些默认的实现,那么可以使用抽象类
  • 如果希望实现多个继承,那么必须使用接口。因为Java中类不支持多继承,但接口支持多继承
  • 当关注的是一个事物的本质则使用抽象类;当关注的是一组操作则使用接口

15.3.11 接口和抽象类

  1. 相同点
  • 接口和抽象类都不能被实例化
  • 都位于继承关系的顶端,用于被其他类来继承或实现
  • 都包含抽象方法,实现子类必须对其中的抽象方法进行实现
  1. 不同点
  • 抽象类主要用来抽象类型,表示的是这个对象是什么;接口用来抽象功能,表示这个对象能做什么
  • 抽象类中可以有抽象方法,也可以有实例方法;接口可以理解为更加抽象的抽象类,接口中不能包含实例方法
  • 抽象类只支持单一继承;接口可以支持多继承(实现)
  • 抽象类中成员的修饰符可以是public、protected和default,接口中默认的修饰符为public,而不能使用其他修饰符(私有方法除外)
  • 抽象类中支持变量;接口中值允许使用常量,默认修饰符是public staticfinal
  • 抽象类中可以丁欧构造器;接口中不能定义构造器
  • 抽象类的子类可以对父类的部分抽象方法进行实现,如果子类没有实现全部的抽象方法,则该子类仍为抽象类;接口的实现类必须对接口中声明的所有方法进行实现。

16 包装类

16.1 拆箱与装箱

16.1.1 基本概念

  • 装箱(Boxing):将一个基本型数据赋值给一个引用类型变量的过程称为装箱
  • 拆箱(UnBoxing):将一个引用类型数据赋值给基本类型变量的过程称为拆箱

16.1.2 示例

java
public static void main(String[] args) {
 //定义基本类型变量
 int i = 20;

 //装箱:将一个基本类型数据赋值给一个引用类型变量
 String str = String.valueOf(i);

 //拆箱:将一个引用类型数据赋值给一个基本类型变量
 int j = Integer.parseInt(str);
}

16.1.3 问题

  • 拆箱过程中可能会存在安全隐患
  • 拆箱与装箱操作过程中会造成一定的性能损失

16.1.4 建议

尽量避免拆箱与装箱操作。

16.2 包装类

16.2.1 为什么要有包装类

  • Java是一个面向对象的编程语言,基本类型的存在导致Java不是一个完全面向对象的语言
  • 基本类型和引用类型之间的转换可能会产生拆箱与装箱操作
  • 在执行类型转换时,需要提供相应的方法来实现转换操作(包装类就提供了转换的方法)

16.2.2 包装类

包装类型基本类型
Bytebyte
Shortshort
Integerint
Longlong
Floatfloat
Doubledouble
Characterchar
Booleanboolean

16.2.3 数值类型

  1. 公共字段
  • MAX_VALUE:返回类型的最大值
  • MIN_VALUE:返回类型的最小值
  • SIZE:返回类型的二进制位数
  • TYPE:返回类型的Class实例
  1. 构造器
  • XXX(xx i):分配一个新的Xxx类型对象
  • Xxx(String s):表示将字符串类型数据分配给一个新的Xxx类型对象

注意:从Jdk1.9开始构造器已经被弃用

  1. 常用方法
  • compareTo():比较两个对象之间值的大小关系
  • equals():比较两个对象之间值是否相等
  • parseXxx():将一个值转为Xxx类型
  • valueOf():将一个值转为Xxx类型(会使用缓存)
  • toString():将当前对象转为String类型数据
  1. 示例
java
public static void main(String[] args) {
 //1、字段
 //MIN_VALUE:返回当前类型的最小值
 System.out.println(Integer.MIN_VALUE);

 //MAX_VALUE:返回当前类型的最大值
 System.out.println(Integer.MAX_VALUE);

 //SIZE:返回当前类型的二进制位数
 System.out.println(Integer.SIZE);

 //TYPE:返回类型的Class类型
 System.out.println(Integer.TYPE);

 //2、构造器
 //构造器从Jdk1.9开始已经不建议使用(弃用)
 //Integer(int):创建一个Integer类型对象,并将值分配给对象
 //Integer i1 = new Integer(20);
 //Integer(String):创建一个Integer类型对象,并将值分配给对象
 //Integer i2 = new Integer("20");

 //3、常用方法
 Integer i3 = 24;
 Integer i4 = 28;
 Integer i5 = 24;

 //compareTo():比较两个Integer类型对象值的大小
 //return (x < y) ? -1 : ((x == y) ? 0 : 1);
 System.out.println(i3.compareTo(i4));

 //equals():比较两个对象的值是否相等
 //return value == ((Integer)obj).intValue();
 System.out.println(i3.equals(i5));

 //parseXxx():将一个数据转为Integer类型数据
 Integer i6 = Integer.parseInt("20");
 //Integer i7 = Integer.parseInt("20a");

 //valueOf():将一个数据转为Integer类型数据(使用缓存)
 Integer i8 = Integer.valueOf(20);
 //Integer i9 = Integer.valueOf("20a");

 String str = i8.toString();
}
  1. 注意

在Integer类型的内部维护了一个缓存,当对象的值在缓存范围内将不创建新的对象,而直接引用缓存中的对象。

相反,如果值超出了缓存的范围,则创建一个新的对象。

java
public static void main(String[] args) {
 //比较结果为true
 //==比较两个对象是否指向了同一个引用,Integer内部维护了一个缓存对象(Integer[])
 //对象将-128~127之间的所有整数都放在了缓存中,所以i1、i2没有创建新对象,而直接使用了缓存内对象
 Integer i1 = 127;
 Integer i2 = 127;
 System.out.println(i1 == i2);

 //比较结果为false
 //==比较两个对象是否指向了同一个引用,Integer内部维护了一个缓存对象(Integer[])
 //对象将-128~127之间的所有整数都放在了缓存中,i3、i4的超出了缓存范围,所以他们两个独立的对象
 Integer i3 = 128;
 Integer i4 = 128;
 System.out.println(i3 == i4);
}

16.2.4 字符类型

  1. 常用方法
  • isDigit():返回当前字符是否为数字
  • isUpperCase():返回当前字符是否为大写字符
  • isLowerCase():返回当前字符是否为小写字符
  • toUpperCase():将当前字符转为大写字符
  • toLowerCase():将当前字符串转为小写字符
  • toString():将一个字符串转为字符串对象

16.2.5 布尔类型

  1. 常用方法
  • getBoolean():将一个字符串值转为对应的布尔值
  • parseBoolean():将一个字符串值转为布尔值

17 String

17.1 概述

String称为字符串类型,是一个不可变的Unicode字符序列。String是被final修饰的最终类,不可以被继承。

java
//类是被final修饰的
//final修饰的类表示最终版本,无法被继承
public final class String implements java.io.Serializable, Comparable<String>, CharSequence, Constable, ConstantDesc {
 /**The value is used for character storage. */
 //value是用来存储字符序列的。在Jdk1.9前使用char[],Jdk1.9开始使用byte数组
 //value被final修饰,final修饰的字段称为常量。其值不可以被改变
 //常量值只能在声明时、代码块或构造器中进行初始化
 `@Stable`
 private final byte[] value;

 /** Cache the hash code for the string */
 private int hash; // Default to 0
}

17.2 创建字符串对象

java
//1、通过字面量方式创建字符串对象
String s1 = "abc";

//2、通过构造器方式创建字符串对象
//1)、byte数组方式
String s2 = new String(new byte[]{97, 98, 99});
System.out.println(s2);

//2)、char数组方式
String s3 = new String(new char[]{'a', 'b', 'c'});
System.out.println(s3);

//3)、字面量方式
String s4 = new String("abc");

17.3 字符串对象的存储

17.4 常用方法

17.4.1 比较

java
String s1 = "Hello Tom";
String s2 = "Hello Tom";
String s3 = "hello tom";

//1、字符串比较
//a、==:比较两个字符串对象是否指向了同一个引用
System.out.println(s1 == s2);

//b、compareTo():按照字典顺序比较两个字符串之间关系
//getChar(value, k) - getChar(other, k)
System.out.println(s1.compareTo(s2));

//c、compareToIgnoreCase():按照字典顺序比较两个字符串之间关系,忽略大小写
System.out.println(s1.compareToIgnoreCase(s3));

//d、equals():比较两个字符串对象是否相等
//原理:先判断是否是同一个引用,如果不是在按照存储顺序逐一判断字符是否相等
//value[i] != other[i] return false
System.out.println(s1.equals(s2));

//e、equalsIgnoreCase():比较两个字符串对象是否相等,忽略大小写
System.out.println(s1.equalsIgnoreCase(s2));

17.4.2 判断

java
//2、字符串的判断
//a、isEmpty():判断字符串对象的内容是否为空(length()==0是返回true)
System.out.println(s1.isEmpty());

//b、contains():判断字符串中是否包含指定的子串
System.out.println(s1.contains("lo"));

//c、startWith():判断字符串是否以指定子串开头
System.out.println(s1.startsWith("He"));

//d、endWith():判断字符串是否以指定子串结尾
System.out.println(s1.endsWith("He"));

17.4.3 查找

java
//3、字符串查找
//a、charAt():返回指定索引位置的字符内容
System.out.println(s1.charAt(2));

//b、indexOf():指定字符串内容首次在当前字符串中出现的索引位置
System.out.println(s1.indexOf("l"));

//c、lastIndexOf():指定字符串内容最后一次在当前字符串中出现的索引位置
System.out.println(s1.lastIndexOf("l"));

17.4.4 处理

java
//4、字符串处理
//a、concat():将一个字符串拼接到当前字符串的末尾(相当于+操作符)
System.out.println(s1.concat("123456"));

//b、replace():使用指定子串替换原有字符串中的指定内容
System.out.println(s1.replace("l", "DD"));

//c、split():使用指定分隔符对字符串内容进行拆分
String[] array = s1.split(" ");
for(String s : array){
 System.out.println(s);
}

//d、subString():从指定索引位置开始对字符串内容进行截取
//从索引2开始截取到末尾
System.out.println(s1.substring(2));

//从索引2开始截取到索引4(包前不包后,第二个索引前一个位置为止)
System.out.println(s1.substring(2, 4));

//e、trim():去除字符串首尾空格
System.out.println(s4.trim());

17.4.5 其他

java
//5、其他函数
//a、getBytes():返回当前字符串对象的byte数组
byte[] b = s1.getBytes();

//b、toCharArray():返回当前字符串的char数组
char[] c = s1.toCharArray();

//c、length():返回当前字符串的长度
System.out.println(s1.length());

//d、toUpperCase():将字符串内容全部转为大写字符
System.out.println(s3.toUpperCase());

//e、toLowerCase():将字符串内容全部转为小写字符
System.out.println(s3.toLowerCase());

//f、valueOf():将其他类型数据转为字符串对象
String s7 = String.valueOf(20);

17.5 字符串的创建

17.5.1 字符串创建方式

  • 字面量方式创建
  • Char[]方式创建
  • Byte[]方式创建
  • 从现有字符串方式创建
  • +拼接方式创建

17.5.2 Byte[]方式

java
//byte[]方式创建字符串对象
String str = new String(new byte[]{97,98,99});

| 【Jdk1.8】
根据ASCII表的对应关系将字母存储到内存中。 | | 【Jdk1.9】 |

通过统计发现在计算机中存储拉丁字符的频率较高,而拉丁字符占用1个字节的存储空间。而char数组默认需要2个字节的存储空间,所以从Jdk1.9开始将字符串的底层实现从char[]转为了byte[],目的是为了更加合理的利用存储空间。

17.5.3 字面量方式创建数组对象

后四种方式创建字符串基本相似,但字面量方式有很大区别。这种方式也是使用的最多的方式。

java
//字面量方式创建字符串对象
String str = "abc";

“abc”就称为字面量(英文:Literal),这种方式是最常用方式,也是内部结构最复杂的方式。

  • 懒加载
  • 不重复
  • 非对象
  1. 非对象
java
//abc仅仅是一个符号,他不是字符串对象
String str = "abc";

当代码执行到赋值语句时,他还不是一个字符串对象。

程序代码执行第一行时,并没有在堆中创建字符串对象,当执行到第2行代码时才会创建值为“abc”的字符串对象。

当class文件加载结束后,“abc”字面量被存储在运行时常量池中(在方法区中),常量池中存储的数据都是键值对。其中#1、#2等在运行时都会被编译为真正的内存地址。

常量池的作用:
在代码执行过程中,如果需要调用其他方法,程序需要知道方法的相关信息,如:方法的所属类、参数及入口地址等。这些信息都可以在常量池中找到。

在class文件中,main()方法有很多执行,这些指令我们称为字节码指令。

代码执行时,常量会被加载到常量池中,代码会被加载到代码区中,执行main()方法时会创建一个主线程。启动主线程时会自动在栈内存空间中创建一个栈帧。栈帧中北湖一个程序计数器来记录代码执行的位置。

程序代码从上向下逐一进行执行,字符串变量进行赋值时,首先去常量池中读取字面量“abc”的值,然后在堆内存空间中创建字符串对象。

  1. 懒加载

代码执行到ldc #7时才会根据字面量的值去创建字符串对象,字符串对象在使用前不会被创建。

  1. 不重复

在类中相同字面量在内存中只有1分。

在代码中2次用到了同一个字面量对象abc,但代码其实都指向了同一个字面量(#7)。这说明他们使用的是同一个字面两对象。

17.5.4 +拼接方式

java
//使用+进行字符串拼接后再进行赋值
String s1 = "a" + "b";

| | | |

通过字节码发现并没有拼接操作的发生。编译后Java就已经把’a’和’b’拼接成了一个新的字符串,这个处理我们称为编译优化。

17.6 StringTable

字符串:

  • 野生的:不受管理的
  • 家养的:受管理的

前面的字符串创建方式中,只有字面量的方式是受管理的(家养的)。字面量方式是受管理的,他可以保证字符串不重复,而不收管理的无法保证同时存在多个重复的字符串对象。

  • 字面量创建的对象将字符串内容存储到StringTable(Hash表)中。StringTable管理字符串对象的不重复,也就是我们说的家养的
  • 其他方式及+拼接方式本质上都采用了new操作符来创建对象。他们都在对中创建新的字符串对象,不会考虑字符串是否重复。这种情况会导致重复字符串的产生

17.7 StringBuilder

17.7.1 概述

StringBuilder是一个可变的字符序列,内部包含了一个具有一定容量的缓冲区,通过方法可以改变字符串序列的长度和内容。StringBuilder是StringBuffer的简易替换,StringBuilder是非线程安全的。在单线程中使用具有较高的性能。

17.7.2 底层实现

java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
 /**
 * The value is used for character storage.
 * value被用来存储字符
 * 缓存区(缓冲区就是内存中开辟的一块空间)
 */
 byte[] value;

 /**
 * The id of the encoding used to encode the bytes in {`@code` value}.
 */
 byte coder;

 /**
 * The count is the number of characters used.
 */
 int count;
}
java
public final class StringBuilder
 extends AbstractStringBuilder
 implements java.io.Serializable, Comparable<StringBuilder>, CharSequence {
 /**
 * Constructs a string builder with no characters in it and an
 * initial capacity of 16 characters.
 * 默认初始容量为16个字符
 */
 `@IntrinsicCandidate`
 public StringBuilder() {
 super(16);
 }
}

说明:StringBuilder的底层是通过数组进行字符串内容存储的。

17.7.3 构造函数

  • StringBuilder():构造一个空字符串缓冲区,初始容量为16字符
  • StringBuilder(int):构建一个具有指定大小的缓冲区
  • StringBuilder(String):构建一个具有默认值的字符串缓冲区

17.7.4 创建对象

java
//构建一个具有初始容量的缓冲区,初始容量为16个字符
StringBuilder sb1 = new StringBuilder();

//构建一个具有指定容量大小的缓冲区,可以有效避免频繁执行扩容操作,提高效率
StringBuilder sb2 = new StringBuilder(18);

//构建一个具有初始值的字StringBuilder对象。缓冲区的容量为:
//长度小于Integer.MAX_VALUE-16,则缓冲区大小=字符串长度+16,
StringBuilder sb3 = new StringBuilder("hello");

17.7.5 常用方法

java
//2、常用Api
//a、append():将字符串内容追加到当前StringBuilder内容的末尾
sb1.append("a");

//b、insert():将字符串内容插入到当前字符串内容的指定索引位置
sb1.insert(0, "ok_");

//c、capacity():返回当前StringBuilder缓冲区的容量
System.out.println(sb1.capacity());

//d、length():返回当前StringBuilder中字符的个数
System.out.println(sb1.length());

//e、delete():移除指定索引范围内的字符内容(包前不包后)
sb1.delete(0, 2);

17.7.6 对象的存储

java
/**
 * StringBuilder对象的存储
 */
public class TestStringBuilder {
 public static void main(String[] args) {
 String s1 = "abc";

 StringBuilder sb = new StringBuilder("abc");
 }
}

| |

17.7.7 StringBuilder转String类型

java
/**
 * StringBuilder对象的存储
 */
public class TestStringBuilder {
 public static void main(String[] args) {
 String s1 = "abc";

 StringBuilder sb = new StringBuilder("abc");

 //1、将String转为StringBuilder
 StringBuilder sb2 = new StringBuilder(s1);

 //2、将StringBuilder转为String
 String s2 = sb2.toString();
 }
}

17.8 StringBuffer

17.8.1 概述

StringBuffer是一个可变的字符序列,内部包含了一个具有一定容量的缓冲区,通过方法可以改变字符串序列的长度和内容。StringBuffer是线程安全的。适用于在多线程环境中进行使用。

17.8.2 构造函数

17.8.3 常用方法

参考StringBuilder

17.9 性能

java
/**
 * 测试String、StringBuilder及StringBuffer
 */
public class TestString {
 public static void main(String[] args) {
 String s1 = "";
 StringBuffer s2 = new StringBuffer();
 StringBuilder s3 = new StringBuilder();

 //获取开始时间
 long start = System.currentTimeMillis();

 for(int i = 0; i < 60000; i++) {
 //s1+="a";
 s2.append("a");
 //s3.append("a");
 }

 long end = System.currentTimeMillis();

 //计算时间差
 System.out.println("用时:" + (end - start));
 }
}

说明:在执行字符串拼接过程中,String的拼接效率相对较低。StringB uffer、StringBuilder的效率相对较高。

17.10 常见问题

  1. 为什么说字符串是一个不可变的字符序列
  2. 简述==与equals()的区别
  3. String有length吗?String有length()吗?
  4. String s1=”xyz”创建了几个对象?String s1=new String(“xyz”)创建了几个对象?
  5. 简述String、StringBulder、StringBuffer的区别。
  • 底层实现:String使用不可变的数组对象存储内容;StringBuffer、StringBuilder使用的是可变数组对象存储内容。
  • 初始容量不同
  • StringBuilder是非线程安全的;StringBuffer是线程安全的

17.11 练习

  1. 对于任意给定的字符串对象,编写方法判断字符串中指定子串重复出现的次数
  2. 对于任意给定的邮件地址,编写方法判断邮件地址是否合法
  3. 编写方法实现对任意字符串内容的加密与解密操作
  4. 对于任意给定的字符串,编写方法统计字符串中数字的个数
  5. 给定一个任意的字符串,字符串中包含括号内容。编写代码判断所有的括号是否成对出现(面试题)。
  • 思路1:定义一个计数器,遇到左括号计数器+1,遇到右括号计数器-1.字符串遍历结束后判断计数器是否为0
  • 思路2:定义一个栈。遇到括号就到栈中进行查找。如果找到匹配的另一半则出栈。找不到则将括号入栈。

18 常用类

18.1 System类

18.1.1 概述

提供了一组静态字段和方法。通过这些字段和方法可以实现输入和输出及系统环境信息的获取。

18.1.2 常用方法

java
/**
 * 1、常用类
 * System类
 */
public class TestSystem {
 public static void main(String[] args) {
 //1、out:PrintStream类型对象。对象中提供了输出的相关方法
 System.out.println("out对象");

 //2、currentTimeMillis():返回当前时间的毫秒数(1970-1-1)
 //通常用于计算时间差
 //1秒=1000毫秒
 System.out.println(System.currentTimeMillis());

 //3、exit():退出当前正在执行的Jvm虚拟机(进程)
 //0--正常退出;非0--非正常退出
 //System.exit(15);

 //4、gc():调用Jvm的垃圾收集器(对堆空间中的内存进行回收)
 //注意:自己不要随意调用gc(),可能会对程序产生性能影响
 System.gc();

 //5、getProperty():返回当前环境信息
 System.out.println(System.getProperty("java.home"));
 System.out.println(System.getProperty("java.version"));
 System.out.println(System.getProperty("os.name"));
 System.out.println(System.getProperty("user.name"));
 }
}

| :--- |

18.2 Math类

18.2.1 概述

Math类中提供了一组用于算术运算的相关方法。

18.2.2 常用方法

java
/**
 * 2、常用类
 * Math类
 */
public class TestMath {
 public static void main(String[] args) {
 //1、常用字段
 // a、PI:π值
 System.out.println(Math.PI);

 //2、常用方法
 //a、abc():返回一个数的绝对值
 System.out.println(Math.abs(8.97));
 System.out.println(Math.abs(-8.97));

 //b、round():返回最接近参数的整数值(四舍五入)
 System.out.println(Math.round(8.97));
 System.out.println(Math.round(-8.97));

 System.out.println(Math.round(8.47));
 System.out.println(Math.round(-8.47));

 //c、ceil():返回大于或等于参数的最小浮点数(所有数值序列中最接近参数的那个值)
 System.out.println(Math.ceil(8.97));
 System.out.println(Math.ceil(-8.97));

 System.out.println(Math.ceil(8.47));
 System.out.println(Math.ceil(-8.47));

 //d、floor():返回小于或等于参数的最小浮点数(所有数值序列中最接近参数的那个值)
 System.out.println(Math.floor(8.97));
 System.out.println(Math.floor(-8.97));

 System.out.println(Math.floor(8.47));
 System.out.println(Math.floor(-8.47));

 //e、max():返回两个参数中较大参数的值
 System.out.println(Math.max(20,77));
 //f、min():返回两个参数中较小参数的值
 System.out.println(Math.min(20,77));

 //g、random():返回一个0~1之间的随机小数
 System.out.println(Math.random());
 }
}

18.3 Jdk1.8-

18.3.1 时间标准

  • UTC(Coordinated Universal Time)
  • GMT(GreenwichMeanTime)
  • CST(Central Standard Time)

18.3.2 Java.util.Date

  1. 概述

Date类表示任意的一个时间点。Date类可以把日期解析为年月日等信息。

  1. 示例
java
/**
 * 3、日期类
 * java.util.Date
 */
public class TestDate {
 public static void main(String[] args) {
 //1、实例化日期对象
 //a、调用无参构造
 Date d1 = new Date();

 //b、调用带参构造
 long millis = System.currentTimeMillis()-60000;
 Date d2 = new Date(millis);

 System.out.println(d1);
 System.out.println(d2);

 //2、常用方法
 //a、after():返回日期是否在指定日期之后
 System.out.println(d1.after(d2));

 //b、before():返回日期是否在指定日期之前
 System.out.println(d1.before(d2));

 //c、compare():比较两个日期之间的大小关系
 //(thisTime<anotherTime ? -1 : (thisTime==anotherTime ? 0 : 1))
 System.out.println(d1.compareTo(d2));

 //d、toInstant():将当前时间转为Instant对象
 System.out.println(d1.toInstant());

 //e、toString():将当前时间转为String对象
 System.out.println(d1.toString());

 //f、其他方法(弃用)
 //getYear():返回当前时间距1900年之间的年份差
 System.out.println(d1.getYear());

 //getMonth():返回当前时间的月份(月份取值范围为:0~11)
 System.out.println(d1.getMonth());

 //getDay():返回当前时间时一周中的第几天
 System.out.println(d1.getDay());

 //getDate():返回当前时间时一个月中的第几天
 System.out.println(d1.getDate());

 //getHours():返回当前时间的小时信息
 System.out.println(d1.getHours());
 }
}

18.3.3 Java.sql.Date

  1. 概述

Java.util.Date的子类,与Jdbc的日期类型相兼容。

  1. 示例
java
/**
 * 4、日期类
 * java.sql.Date
 */
public class TestDate {
 public static void main(String[] args) {
 //1、对象实例化
 long l = System.currentTimeMillis();
 Date d = new Date(l);
 System.out.println(d);
 System.out.println("l:" + l);

 //2、常用方法
 System.out.println(d.getYear());

 //valueOf():将一个日期格式字符串转为日期对象
 Date d2 = Date.valueOf("2020-3-30");
 System.out.println(d2);

 //getTime():返回当前时间的时间差
 long time = d.getTime();
 System.out.println(time);
 }
}
  1. 日期间转换
java
/**
 * 日期类型之间的转换
 */
public class TestConvert {
 public static void main(String[] args) {
 //util.Date
 java.util.Date d1 = new java.util.Date();

 //java.util.Date-->java.sql.Date
 java.sql.Date d2 = new java.sql.Date(d1.getTime());
 System.out.println(d2);

 //java.sql.Date-->java.util.Date
 //直接赋值缺少时间信息
 java.util.Date d3 = d2;
 java.util.Date d4 = new java.util.Date(d2.getTime());
 System.out.println(d4);
 }
}

18.3.4 Calendar

  1. 概述

Calendar是一个抽象类,提供了日期信息转换的相关方法。

  1. 快捷键

Ctl+h:查看继承关系

  1. 继承关系

  1. 示例
java
//1、实例化对象
Calendar c = new GregorianCalendar();
System.out.println(c);

//2、常用字段
//a、YEAR:为get/set方法提供的属性字段值
System.out.println(Calendar.YEAR);
System.out.println(Calendar.MONTH);
System.out.println(Calendar.WEEK_OF_MONTH);

//3、常用方法
//a、add():在日历的指定字段上加上一个值
c.add(Calendar.YEAR, 10);
c.add(Calendar.YEAR, -30);
System.out.println(c);

//b、getActualMaximum():获取日历信息中指定字段的真实最大值
System.out.println(c.getActualMaximum(Calendar.MONTH));
System.out.println(c.getActualMaximum(Calendar.WEEK_OF_YEAR));

//c、getMaximum():获取日历信息中指定字段的最大值
System.out.println(c.getMaximum(Calendar.MONTH));
System.out.println(c.getMaximum(Calendar.WEEK_OF_YEAR));

//d、get():获取日期信息中的指定字段的信息
System.out.println(c.get(Calendar.YEAR));
System.out.println(c.get(Calendar.MONTH));
System.out.println(c.get(Calendar.WEEK_OF_MONTH));
System.out.println(c.get(Calendar.DATE));

//e、getTime():返回当前日期和时间信息
System.out.println(c.getTime());

//f、getTimeInMillis():返回当前时间的时间差
System.out.println(c.getTimeInMillis());

18.3.5 SimpleDateFormat

  1. 概述

SimpleDateFormat是一个与语言环境有关的日期格式化器,类中提供了格式化和日期解析的相关方法。

  1. 示例
java
/**
 * 6、日期类
 * SimpleDateFormat
 */
public class TestSimleDateFormat {
 public static void main(String[] args) throws ParseException {
 //1、实例化格式化器
 SimpleDateFormat sdf1 = new SimpleDateFormat();
 SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
 SimpleDateFormat sdf3 = new SimpleDateFormat("yyyy年MM月dd日 hh:mm");
 SimpleDateFormat sdf4 = new SimpleDateFormat("yy-M-d h:m");

 //2、实例化日期对象
 Date d = new Date();
 System.out.println(d);

 //3、使用格式化器
 //a、format():对日期进行格式化
 System.out.println(sdf1.format(d));
 System.out.println(sdf2.format(d));
 System.out.println(sdf3.format(d));
 System.out.println(sdf4.format(d));

 //b、parse():将一个日期字符串转为日期对象
 String str1 = "2023-1-17 10:23:8";
 Date d2 = sdf2.parse(str1);
 System.out.println(d2);

 //c、toPattern():将格式化信息转为字符串对象
 System.out.println(sdf3.toPattern());
 }
}

18.4 Jdk1.8+

18.4.1 LocalDate

java
/**
 * 7、日期类
 * LocalDate(相当于java.sql.Date)
 *
 */
public class TestLocalDate {
 public static void main(String[] args) {
 //实例化对象
 LocalDate d1 = LocalDate.now();
 System.out.println(d1);

 //获取日期的相关信息
 System.out.println(d1.getYear());
 System.out.println(d1.getMonth());
 System.out.println(d1.getDayOfMonth());

 System.out.println(d1.get(ChronoField.YEAR));
 }
}

18.4.2 LocalTime

java
/**
 * 8、日期类
 * LocalTime
 */
public class TestLocalTime {
 public static void main(String[] args) {
 //1、实例化对象
 LocalTime t = LocalTime.now();
 System.out.println(t);

 //2、常用方法
 System.out.println(t.getHour());
 System.out.println(t.getMinute());
 System.out.println(t.getSecond());

 LocalTime t2 = LocalTime.of(13,18);
 System.out.println(t2);

 }
}

18.4.3 LocalDateTime

java
/**
 * 9、日期类
 * LocalDateTime(LocalDateTime = LocalDateTime + LocalTime)
 */
public class TestLocalDateTime {
 public static void main(String[] args) {
 LocalDateTime d = LocalDateTime.now();
 System.out.println(d);
 }
}

18.4.4 Instant

java
/**
 * 10、日期类
 * Instant
 */
public class TestInstant {
 public static void main(String[] args) {
 Instant d = Instant.now();

 System.out.println(d);
 }
}

18.5 练习

  1. 编写代码实现万年历效果。需求如下:
  • 用户从键盘输入年份后,在控制台显示本年度全年日历信息
  1. 编写一个日期工具类,基本需求如下:
  • 提供方法实现日期格式化功能
  • 提供方法将日期字符串转为日期格式的对象
  • 提供方法将日期字符串转为日期时间格式的对象

19 自定义比较化器

19.1 为什么要实现自定义比较化器

使用Arrays.sort()对数组对象进行排序时,Java提供的类型可以实现排序,而自定义类型调用Arrays.sort()方法对数组进行排序时产生异常。因为Java中提供的类型内部实现了比较方式(比较化器),而自定义类型没有提供比较方式。

19.2 默认排序

  • number:按照大小排序
  • char:按照字符编码排序
  • String:按照字母顺序进行排序
  • Boolean:true大于false

19.3 分类

  • 自然排序
  • 定制排序

19.4 自然排序

19.4.1 步骤

  • 创建类并实现Compareable接口
  • 实现接口方法
  • 使用类

19.4.2 示例

java
【实体类】
/**
 * 用户类
 */
public class User implements Comparable<User> {
 private int id;
 private String name;
 private boolean gender;

 public User() {
 }

 public User(int id, String name, boolean gender) {
 this.id = id;
 this.name = name;
 this.gender = gender;
 }

 //get/set访问器


 /**
 * 自定义比较方法
 * 0-相等;负值-当前对象小于参数对象;正值-当前对象大于参数对象
 */
 `@Override`
 public int compareTo(User o) {
 //判断对象o是否为null
 if(o==null)
 return -1;

 //按照ID排序
 //return o.id-id;
 //按照Name字段排序
 //return o.name.compareTo(name);
 //按照性别进行排序
 return Boolean.compare(gender,o.gender);
 }

 `@Override`
 public String toString() {
 return "User{" +
 "id=" + id +
 ", name='" + name + '\'' +
 ", gender=" + gender +
 '}';
 }
}
java
【测试代码】
/**
 * 测试自然排序
 */
public class TestSort {
 public static void main(String[] args) {
 User[] arr = {
 new User(1,"Tom",true),
 new User(9,"Marray",false),
 new User(3,"Mike",true),
 new User(7,"Bob",true)
 };

 //调用Arrays类的sort()方法进行排序
 Arrays.sort(arr);

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

19.4.3 特点

  • 直接对类进行调用就可以
  • 比较方式统一

19.5 定制排序

19.5.1 步骤

  • 以匿名内部类方式实现Comparator接口
  • 实现接口方法
  • 将接口对象作为参数进行传递

19.5.2 示例

java
【实体类】
/**
 * 用户类
 */
public class User{
 private int id;
 private String name;
 private boolean gender;

 public User() {
 }

 public User(int id, String name, boolean gender) {
 this.id = id;
 this.name = name;
 this.gender = gender;
 }

 //get/set访问器


 `@Override`
 public String toString() {
 return "User{" +
 "id=" + id +
 ", name='" + name + '\'' +
 ", gender=" + gender +
 '}';
 }
}
java
【测试类】
/**
 * 测试定制排序
 */
public class TestSort {
 public static void main(String[] args) {
 User[] arr = {
 new User(1,"Tom",true),
 new User(9,"Marray",false),
 new User(3,"Mike",true),
 new User(7,"Bob",true)
 };

 //调用Arrays类的sort()方法进行排序
 Arrays.sort(arr,new Comparator<User>() {
 `@Override`
 public int compare(User o1, User o2) {
 if(o1==o2)
 return 0;

 return o1.getId()-o2.getId();
 }
 });

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

19.5.3 特点

  • 灵活

19.6 自然排序与定制排序

  • 自然排序需要实现Compareable接口;定制排序实现的是Com parator接口
  • 自然排序在被比较类中进行实现;定制排序是在方法中作为参数方式进行实现
  • 自然排序可以做到同一;定制排序的特点是灵活

20 异常

20.1 概述

异常是指程序运行过程中出现的不可预知的错误。异常将导致程序的运行不按照预期设想进行执行。

java
/**
 * 计算两个数相除的结果
 */
public class TestDiv {
 public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);

 //同样可能会出现错误
 System.out.print("运算数1:");
 int num1 = sc.nextInt();
 System.out.print("运算数2:");
 int num2 = sc.nextInt();

 //通过判断的方式来解决异常(为代码打补丁)
 if(num2==0)
 System.out.println("警告:运算数2不能为0");
 else
 //输出运算结果
 System.out.println(num1 + "÷" + num2 + "=" + (num1/num2));
 }
}

20.2 异常的体系结构

  • Throwable:异常类型的基类,是所有异常和错误的父类。只有该类或其子类实例对象才能被jvm虚拟机捕获或被throw进行抛出。只有该类或其子类实例才能被catch语句捕获。
  • Error:表示程序无法捕获的严重错误(即使我们捕获也无法进行处理)。如:堆栈溢出等。
java
/**
 * Error异常
 */
public class TestError {
 public static void main(String[] args) {
 method01();
 }

 //方法对自身进行重复调用(栈溢出)
 private static void method01() {
 method01();
 }
}
markdown
Exception in thread "main" java.lang.StackOverflowError
  • Exception:表示程序运行时产生的异常,Exception异常时可以被捕获的,同时也希望被捕获。否则将影响程序的正常执行。
java
/**
 * Exception异常
 */
public class TestException01 {
 public static void main(String[] args) {
 int[] arr = new int[4];

 //程序运行产生异常:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
 //Index 10 out of bounds for length 4
 //异常原因:超出数组的最大索引下标(数组的下标越界)
 arr[10]= 1;
 }
}

20.3 异常的分类

  1. 编译时异常(受检异常(Checked))

编译时异常在代码的编译阶段就需要程序员进行处理,这类异常一般后果相对比较严重。

java
/**
 * 编译时异常(受检异常)
 */
public class TestCheckedException {
 public static void main(String[] args) {
 //受检异常(在代码的编写及编译阶段就会要求进行处理)
 try {
 InputStream is = new FileInputStream("");
 } catch (FileNotFoundException e) {
 throw new RuntimeException(e);
 }
 }
}
  1. 运行时异常(非受检异常(UnChecked))

运行时异常在程序编译阶段不会产生,在程序运行期间可能会因为其他原因而产生异常。

java
/**
 * 运行时异常(非受检异常)
 */
public class TestUnCheckedException {
 public static void main(String[] args) {
 //运行时遗产随代码的执行,可能会产生异常
 //如果运算数2不是0则可能永远不会产生异常
 System.out.println(20/0);
 }
}

| :--- |

20.4 语法

20.4.1 语法

java
Try{
 [代码段1]
}
Catch(异常对象){
 [代码段2]
}
Finally{
 [代码段3]
}

20.4.2 说明

  • try语句段中定义可能出现异常的代码,catch语句用于对捕获到的异常进行处理,finally中定义资源释放的代码
  • try语句不能独立使用,必须和catch或finally连用
  • 一个try语句块可以有多个catch语句,大的异常类型必须放在小异常类型的后面

20.4.3 示例

java
/**
 * try...catch语句实现异常处理
 */
public class TestTryCatch {
 public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);

 try {
 //提示并接收用户的键盘输入
 System.out.print("运算数1:");
 int num1 = sc.nextInt();
 System.out.print("运算数2:");
 int num2 = sc.nextInt();

 //执行运算操作
 System.out.println(num1 + "÷" + num2 + "=" + (num1 / num2));
 }
 catch (ArithmeticException ex){
 System.out.println("除数不能为0");
 }
 catch (InputMismatchException ex){
 System.out.println("输入数据错误");
 }
 }
}

20.4.4 执行顺序

  1. 执行顺序
java
/**
 * 测试try语句的执行顺序
 */
public class TestTryCatch {
 public static void main(String[] args) {
 int num1 = 5;
 int num2 = 2;

 try{
 System.out.println("try语句段被执行了.......");

 int result = num1/num2;

 //输出运算结果
 System.out.println("result=" + result);
 }
 catch (Exception ex){
 System.out.println("catch语句段被执行了.......");
 }
 finally {
 System.out.println("finally语句段被执行了.......");
 }
 }
}

| |

  1. 过程描述
  • 程序从try语句开始进行执行,当try语句段中产生异常时,程序将从异常位置转到catch语句段进行执行(try语句段中异常后面的代码不会被执行)
  • catch代码段主要负责对捕获到的异常进行处理。只有当捕获到异常时catch语句段的代码才会被执行
  • finally代码段主要负责执行资源释放,无论是否产生异常,finally语句都会被执行
  1. 注意
  • 当try、catch、finally中同时存在return语句时,只有finally中的return语句可以被执行
  • 当try、catch语句中存在return语句时,在finally语句执行结束后,程序再回到代码块中执行return语句(先执行finally,在执行return语句)

20.5 异常的捕获

Java中的异常是“抓抛式”。当执行引擎对代码进行执行时,代码遇到没有处理的异常,则生成一个异常对象并抛出给代码运行时系统。

20.5.1 异常处理

java
public static void main(String[] args) {
 a();
} //方法a
private static void a() {
 b();
} //方法b
private static void b() {
 c();
} //方法c
private static void c() {
 //会产生一个异常
 //执行引擎将实例化一个异常类型对象
 System.out.println(20/0);
}
``` |
 --- |


+ 执行引擎遇到异常时,如果代码中未对异常进行处理,则将异常向上进行抛出(抛给方法的调用者)
+ 如果方法调用者仍未对异常进行处理,则继续向上抛出。直到最后一级调用者(main()方法)
+ 如果main()方法中仍未对异常进行处理,则将异常交给运行时系统进行处理

### 20.5.2 异常对象的产生
1. 异常对象的产生
 - 自动生成:Jvm虚拟机执行代码遇到异常时将自动创建一个异常对象并进行抛出
 - 手动创建:Java中允许以new的方式手动创建一个异常对象,然后通过throw语句向上进行抛出(将异常抛给方法的调用者)
2. Throw语句

Throw语句用于将一个异常对象向上进行抛出。

1. 示例

```java
/**
 * 查询指定索引位置的元素对象
 */
private static String getValue(int index){
 String[] names = {"Tom","Jerry","Mike","Lucy"};

 //判断索引是否在合理范围内
 if(index < 0 || index >= names.length)
 //手动创建异常对象时,如果创建的是受检异常,则必须进行处理
 //throw:用于将异常对象向上进行抛出
 throw new RuntimeException("索引超出范围");

 return names[index];
}

20.5.3 Throw与throws

  1. Throw
  • 作用

显式的将一个异常对象向上进行抛出。抛给方法的调用者。

  • 语法
java
Throw 异常对象
  • 应用场景
  • 消息超出了方法的返回值范围
java
public int codePointBefore(int index) {
 int i = index - 1;
 if (i < 0 || i >= length()) {
 throw new StringIndexOutOfBoundsException(index);
 }
 if (isLatin1()) {
 return (value[i] & 0xff);
 }
 return StringUTF16.codePointBefore(value, index);
}
  • 返回值的内容与方法的返回值类型不一致
java
public User login(String account,String pwd){
 //返回值与方法的返回类型不一致,使用throw抛出异常对象
 if(account.length() < 1)
 throw new RuntimeException("账号名不能为空");
}
  1. Throws
  • 作用

用于说明方法执行后可能会产生的异常,可以是一个异常列表。

java
//throws用于在方法的签名中说明方法内部可能会产生的异常类型
//如果throw后声明的是受检异常,则方法的调用者必须对方法中可能产生的异常进行处理
private static void a(int i) throws IOException, ArithmeticException {
 if(i % 2 == 0)
 throw new ArithmeticException("这是异常1");
 else
 throw new IOException("这是异常2");
}

| :--- |

  • 使用场景
  • 方法中不知道该如何对异常进行处理,可以通过throws的方法在方法的后面声明可能会产生的异常类型
  • 接口中的抽象方法定义可以预先声明方法内可能产生的异常类型
  • 说明
  • Thows后可以使用具体的异常类型,也可以是异常的父类型

20.6 自定义异常类

20.6.1 Exception类

  1. 构造器
  • Exception():构建一个消息内容为null的新异常对象
  • Exception(String):构建一个包含详细消息内容的新异常对象
  1. 常用方法
  • getMessage():返回异常的消息内容
  • getStackTrace():将异常的消息内容输出到标准文档流
  1. 示例
java
/**
 * 异常的常用方法
 */
public class TestException {
 public static void main(String[] args) {
 try{
 System.out.println(20/0);
 }
 catch (ArithmeticException ex) {
 //printStackTrace():打印异常消息到标准文档流中
 //ex.printStackTrace();
 //getMessage():返回异常消息的内容
 System.out.println(ex.getMessage());
 }
 }
}

20.6.2 常见异常类

  • ClassNotFoundException:类未被发现(依赖没有添加)
  • FileNotFoundException:文件未被发现
  • ParseException:类型转换异常
  • IndexOutOfBoundsException:索引超出范围

20.6.3 自定义异常类

  1. 概述

在自定义或加或系统中需要用到特定的异常类型,则可以通过继承的方式来扩展异常类型。

  1. 实现方式
java
[修饰符] class 类名 extends 异常类型{
 [代码]
}
  1. 示例
java
【自定义异常类】
/**
 * 自定义异常类
 * 步骤:
 * 1、创建类并继承与指定的异常类型(通常情况下继承于RuntimeException)
 * 2、实现异常类的构造函数
 * 3、使用自定义异常类
 */
public class LoginException extends RuntimeException{
 /**
 * 添加构造函数
 * `@param` message
 */
 public LoginException(String message) {
 super(message);
 //可以同时将异常消息记录到日志中
 }
}
java
【使用自定义异常类】
/**
 * 实现用户登录
 * `@param` account
 * `@param` pwd
 * `@return`
 */
 private static User login(String account, String pwd){
 //判断账号名是否正确
 if(!"admin".equals(account))
 throw new LoginException("账号名不存在");
 else{
 //判断密码是否正确
 if(!"123".equals(pwd))
 throw new LoginException("密码错误");
 else
 //User对象应该是从数据库中查询得到
 return new User();
 }
 }
java
【User类】
class User{}

20.7 使用建议

  • try语句段中能定义可能出现异常的代码,大的try代码块可能会导致程序性能的下降
  • catch后尽量使用详细的异常类型(效率会有所提高)
  • 当有多个catch语句块,大的异常类型要方法小异常类型的后面
  • 一个语句块中最多只能有一个try、finally,但可以有多个catch

20.8 意义

  • 提高程序的健壮性
  • 用户提示更加友好

20.9 问题

  1. 简述异常的体系结构
  2. 请说一说Error和Exception的区别
  3. 简述throw和throws的区别
  • Throw出现的方法的内部;throws出现在方法的签名中
  • Throw用于向上抛出一个异常对象;而throws用于声明方法内可能出现的异常类型

21 多线程

21.1 概念

  • 程序(Program):实现了特定功能的一组指令集合,指令是静态的。所以程序是一个静态对象
  • 进程(Process):程序的一次执行过程,或一个正在执行的程序。程序有自己的创建、存在和销毁的过程,这个过程称为声明周期
  • 线程(Thread):一个正在执行的任务路径就是一个线程
  • 同一个进程中同一时间并行执行多任务,就是多线程
  • 线程作为最小的调用和执行单位,每个线程都拥有自己独立的运行栈和程序计数器

21.2 Cpu与多线程

21.2.1 单核Cpu

单核Cpu执行多线程任务时,并不是真正的多线程。而是一种虚拟的多线程。

单核Cpu执行多线程任务相当于一个人一边做饭,同时做菜。表面上同时完成了多件事,而实际上是把时间进行了划分。

单核Cpu执行多线程任务时,将Cpu的执行时间划分成非常小的时间片,然后多个线程争夺Cpu的使用权。拿到Cpu使用权 的线程开始执行任务,指定一定时间后交出Cpu的使用权,然后回到等待队列重新参加Cpu的使用权。

21.2.2 多核Cpu

多核Cpu执行多线程任务时才能真正的发挥多线程的效果。因为每个线程相当于拥有一个独立的Cpu。线程执行过程中,每个核(独立的Cpu)可以一个线程任务。

21.3 并行与并发

  • 并行:多个Cpu同时执行多线程任务。相当于银行储蓄窗口,同时开放了多个业务办理窗口一样。
  • 并发:1个Cpu同时执行多线程任务。相当于一个人同时完成做饭、摘菜、洗菜、打扫卫生等工作。

21.4 优点

  • 同时执行多个不同的任务路径,提高了程序的响应。也提高了用户的体验性
  • 改善了程序的机构,按照职责将任务划分为多个独立的线程,独立执行,便于修改
  • 提高了资源(Cpu)的利用率

21.5 应用场景

  • 程序同时需要执行两个(或以上)任务
  • 程序中某些功能需要处于等待状态。比如:网络中的文件传输
  • 其中部分任务需要在后台执行。比如:下载工具、游戏中的前端界面负责效果的渲染,后台代码负责处理数据

21.6 创建多线程

21.6.1 继承Thread类

  1. Thread类

类可以通过继承java.lang.Thread类来实现多线程。

  1. 构造函数
  • Thread()
  • Thread(Runnable)
  • Thread(String)
  1. 实现步骤
  • 创建类并继承与Thread类
  • 重写类的run()方法
  • 实例化线程对象
  • 调用对象的start()方法启动线程
  1. 示例
java
【自定义线程】
/**
 * 自定义线程
 * 实现步骤:
 * a、创建类并继承于Thread类
 * b、重写类的run()方法
 */
public class MyThread extends Thread{
 /**
 * 线程方法,线程启动后将调用该方法
 * 方法内定义线程需要完成的任务
 */
 `@Override`
 public void run() {
 for(int i = 0;i<1000;i++){
 System.out.println("MyThread.i = " + i);
 }
 }
}
java
【测试】
/**
 * 继承方式创建线程类
 */
public class TestThread {
 public static void main(String[] args) {
 MyThread t = new MyThread();

 //不能直接调用线程对象的run()方法来启动线程
 //如果直接调用了线程对象的run()方法则将成为方法的调用
 //t.run();
 //调用start()方法来启动线程
 t.start();

 for(int j = 0;j<1000;j++){
 System.out.println("main.j = " + j);
 }
 }
}
  1. 注意事项
  • 线程的启动一定通过start()方法来完成,一定不能直接使用run()方法

21.6.2 实现Runnable接口

  1. 概述

线程可以通过实现Runnable接口实现自定义线程类,在线程类中需要对方法进行实现。

  1. 实现步骤
  • 创建类并实现Runnable接口
  • 实现接口的run()方法
  • 使用自定义线程类
  • 实例化Thread类的对象
  • 将自定义线程类的对象作为参数传递给Thread类的构造器
  • 调用Thread类对象的start()方法启动线程
  1. 示例
java
【自定义线程类】
/**
 * 自定义线程类--实现Runnable接口方式
 * 实现步骤:
 * 创建类并实现Runnable接口
 * 实现线程类的run()方法
 */
public class Mythread implements Runnable{
 /**
 * 线程任务方法
 */
 `@Override`
 public void run() {
 for(int i = 0;i<1000;i++){
 System.out.println("MyThread--i= " + i);
 }
 }
}
java
【测试类】
/**
 * 测试线程类
 */
public class TestRunnable {
 public static void main(String[] args) {
 MyThread t = new MyThread();

 //将自定义线程类对象作为Thead构造器的参数进行传递
 Thread t1 = new Thread(t);
 t1.start();

 Thread t2 = new Thread(t);
 t2.start();
 }
}

21.6.3 继承与实现

  • Java 类中只允许单一继承,接口的实现方式可以有效的解决单一继承的约束
  • 线程需要共享资源时,接口的实现方式非常合适

21.7 线程调度

21.7.1 常用方法

  • CurrentThread():返回当前正在执行的线程对象的引用
  • GetId():返回当前线程的表示符(ID)
  • getName():返回当前线程的名称
  • getPriority():返回当前线程的优先级
  • getState():返回当前线程的状态
  • setPriority():设置当前线程的优先级
  • join():等待其他线程的加入
  • sleep():当前线程休眠
  • yield():暂停当前正在执行的线程,让其他线程执行

21.7.2 调度策略

  1. 时间片策略

将Cpu的执行时间划分为较小的时间片,线程相互交替执行。

  1. 优先级策略(抢占式)

优先级高的线程具有更多的执行权。

21.7.3 线程的优先级

java
/**
 * The minimum priority that a thread can have.
 */
public static final int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public static final int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public static final int MAX_PRIORITY = 10;

说明:

  • 继承方式实现线程时,将默认继承父类的优先级
  • 优先级低的线程获取Cpu的控制权的概率低一些,不代表完全没有机会

21.8 线程的生命周期

21.8.1 操作系统层面

首先我们从操作系统层面描述线程的生命周期。

  • 初始状态:线程对象仅在语言层面上进行了创建,目前还没有和操作系统线程进行关联
  • 可运行状态:也称为就绪状态,指线程已经被创建。且与操作系统线程向关联。可以有Cpu进行调度执行了
  • 运行状态:线程获取到Cpu的时间片,正在运行中
  • 当Cpu时间片用完后,会重新从“运行状态”切换到“可运行状态”。这个过程会导致线程上下文切换
  • 阻塞状态:
  • 如果调用了线程的阻塞的相关方法,比如:BIO读写文件,这时线程实际上不适用Cpu。这将导致线程上下文切换,进入到阻塞状态
  • BIO读写操作完成后,有操作系统唤醒阻塞的线程。转换至“可运行状态”
  • 终止状态:表示线程已经执行完成。线程的生命周期结束。不会再转为其他状态

21.8.2 线程的状态(六种)

  1. 线程的状态

从Java Api角度来说明线程的状态。Java中线程的状态是通过枚举对象进行实现的。

java
public enum State {
 /**
 * Thread state for a thread which has not yet started.
 */
 NEW,

 /**
 * Thread state for a runnable thread. A thread in the runnable
 * state is executing in the Java virtual machine but it may
 * be waiting for other resources from the operating system
 * such as processor.
 */
 RUNNABLE,

 /**
 * Thread state for a thread blocked waiting for a monitor lock.
 * A thread in the blocked state is waiting for a monitor lock
 * to enter a synchronized block/method or
 * reenter a synchronized block/method after calling
 * {`@link` Object#wait() Object.wait}.
 */
 BLOCKED,

 /**
 * Thread state for a waiting thread.
 * A thread is in the waiting state due to calling one of the
 * following methods:
 * <ul>
 * <li>{`@link` Object#wait() Object.wait} with no timeout</li>
 * <li>{`@link` #join() Thread.join} with no timeout</li>
 * <li>{`@link` LockSupport#park() LockSupport.park}</li>
 * </ul>
 *
 * <p>A thread in the waiting state is waiting for another thread to
 * perform a particular action.
 *
 * For example, a thread that has called {`@code` Object.wait()}
 * on an object is waiting for another thread to call
 * {`@code` Object.notify()} or {`@code` Object.notifyAll()} on
 * that object. A thread that has called {`@code` Thread.join()}
 * is waiting for a specified thread to terminate.
 */
 WAITING,

 /**
 * Thread state for a waiting thread with a specified waiting time.
 * A thread is in the timed waiting state due to calling one of
 * the following methods with a specified positive waiting time:
 * <ul>
 * <li>{`@link` #sleep Thread.sleep}</li>
 * <li>{`@link` Object#wait(long) Object.wait} with timeout</li>
 * <li>{`@link` #join(long) Thread.join} with timeout</li>
 * <li>{`@link` LockSupport#parkNanos LockSupport.parkNanos}</li>
 * <li>{`@link` LockSupport#parkUntil LockSupport.parkUntil}</li>
 * </ul>
 */
 TIMED_WAITING,

 /**
 * Thread state for a terminated thread.
 * The thread has completed execution.
 */
 TERMINATED;
}
  1. 说明
  • 新建(New):线程对象已经通过new操作符进行实例化,但还没有调用start()方法
  • 运行状态(Runnable):Java线程的就绪状态(Ready)和运行状态(Running)两种状态统称为”运行状态”。也就是说线程可能正在运行,也可能是正在等待Cpu的时间片
  • 阻塞状态(BLOCKED):表示线程被阻塞(等待获取一个排它锁,如果其他线程释放了所就会结束次状态。)
  • 等待(WAITING):等待其他线程显式唤醒,否则将不会被分配Cpu时间片
  • 调用无参的wait()方法
  • 调用无参的join()方法
  • 超时等待(Timed Waiting):超时等待可以在指定时间后自动返回
  • 调用sleep()方法后线程将进入到限期等待状态。
  • 调用带参wait()方法可以使线程进入到限期等待(通常用挂起进行描述)。
  • 销毁(Terminated):表示线程任务执行结束
  • 线程执行结束
  • 产生异常
  • Jvm虚拟崩
线程对象创建后,由其他线程(main线程)调用该对象的start()方法,该线程的状态将处于可运行线程池中,线程将等待被线程调度选中并获取Cpu的使用权。此时的线程就处于就绪状态(Read y)。就绪状态的线程获得Cpu的时间片后立刻变成运行状态(Runn ing).

21.8.3 线程的状态转换

21.9 线程同步

21.9.1 需求

账户有1000元,丈夫和妻子同时向外取钱,每次只允许取500元,直到账户余额为0停止取钱。编写代码模拟取款过程。

21.9.2 实现

java
【取款线程】
/**
 * 线程方式模拟取款
 */
class TackThead implements Runnable{
 //账户余额
 private int money = 10000;

 `@Override`
 public void run() {
 while(money > 0){
 System.out.println(Thread.currentThread().getName() + "取款后账户余额为:" + money);
 //账户余额减少
 money -= 500;
 }
 }
}
java
【测试类】
/**
 * 账户有10000元,丈夫和妻子同时向外取钱,每次只允许取500元,直到账户余额为0停止取钱。编写代码模拟取款过程。
 */
public class TestSync01 {
 public static void main(String[] args) {
 TackThead t = new TackThead();

 //实例化Thread类对象
 Thread t1 = new Thread(t, "丈夫");
 Thread t2 = new Thread(t, "妻子");

 //启动线程
 t1.start();
 t2.start();
 }
}

21.9.3 运行结果

21.9.4 问题描述

运行结果中出现了重复数据。偶尔也会出现数据丢失的情况。

21.9.5 问题分析

当多线程操作一个共享数据时,一个线程对多条语句的执行过程中,只有部分代码被执行了。这时,另一个线程也可以对数据进行执行。这将导致其他线程的操作共享数据前出现了重复操作。

21.9.6 解决问题

对于共享数据进行设置,让同一时刻只能有一个线程对其进行操作。在执行过程中其他线程无法参与操作。

Java中对于多线程的线程安全性问题可以通过特定的方式进行解决:同步机制。

21.9.7 线程同步

  1. 同步代码块
java
Synchronized(锁对象){
 [代码块]
}
  1. 同步方法
java
[修饰符] synchronized 返回值 方法名([参数列表]){
 [方法体]
}
  1. 示例
java
【同步代码块模拟取款】
/**
 * 同步代码块方式模拟取款操作
 */
class TackThread implements Runnable{
 private int money = 10000;

 `@Override`
 public void run() {
 while(money > 500){
 tack();
 }
 }

 /**
 * 同步代码块方式实现取款操作
 */
 private synchronized void tack() {
 //取款金额为500元
 money -= 500;

 System.out.println(Thread.currentThread().getName() + "取款后账户余额为:" + money);
 }
}
java
【测试】
public class TestSync02 {
 public static void main(String[] args) {
 TackThread t = new TackThread();

 //实例化Thread类对象
 Thread t1 = new Thread(t, "丈夫");
 Thread t2 = new Thread(t, "妻子");

 //启动线程
 t1.start();
 t2.start();
 }
}

| |

  1. 同步机制

在多线程中防止多个线程同时访问一个资源(共享资源竞争)。从而造成资源的访问冲突情况。为了解决这个问题,在资源上对其加锁。

加锁后资源在同一个时刻只能有一个线程对其进行访问,直到锁被释放后其他线程才能对其进行访问(加锁后相当于单线程进行访问)。

  1. Synchronized锁
  • 任何对象都可以作为同步锁,所有对象都自动包含一个单一的锁(监视器)
  • 同步方法:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定锁对象,可以指定为this或类名.class
  1. 注意事项
  • 使用共享资源时,多个线程必须使用同一个锁。否则将导致锁不能保证资源的安全性
  • 一个线程类的静态方法有公共的同一个锁(类名.class),所有非静态方法共用一个锁(this)
  1. 同步范围
  • 如何决定是否使用同步锁
  • 确定是否执行多线程代码
  • 是否有共享数据
  • 线程中是否有多行代码需要操作共享数据
  • 解决
  • 操作共享数据的多条语句,只能让一个线程执行,执行过程中不能让其他线程参与执行
  • 直白的说就是操作共享数据的代码都放在同步代码块
  • 说明
  • 范围太大:无法发挥多线程的功能
  • 范围太小:没锁住,仍然存在线程安全性问题
  1. 锁释放
  • 当前同步方法或代码块执行结束
  • 当前线程在同步方法或代码块中出现了未处理异常(Error、Exception),将导致线程异常结束
  • 当前线程在同步方法或代码块中遇到return或break等终止语句时,将导致方法无法继续执行
  • 当前线程在同步方法或代码块中执行了wait()方法时,当先线程将暂停并释放锁
  1. 锁无法释放
  • 线程执行同步方法或代码块时,执行了sleep()、yield()等方法来暂停当前线程的执行
  • 线程执行同步方法或代码块时,其他线程调用了该线程的susp end()方法将该线程挂起,该线程不会释放锁
  • 所以应该尽量避免使用suspend()或resume()来控制线程
  1. 线程的死锁
  • 概述

线程A和线程B分别占用了对方需要进行同步的资源不释放。两个线程都在等待对方释放资源。这种情况就称为死锁。

  • 危害

出现死锁后不会产生异常,也不会进行提示。当前所有线程都将处于阻塞状态,无法继续执行。

  • 解决
  • 尽量减少同步资源
  • 尽量避免嵌套同步
  • 使用合适的算法

21.10 Sleep线程休眠

21.10.1 作用

让当前线程休眠定义的时间。

21.10.2 示例

java
【自定义线程】
/**
 * 自定义线程
 */
class MyThread extends Thread{
 `@Override`
 public void run() {
 for(int i = 1; i < 10; i++){
 //如果i%5==0则让线程休眠5秒
 if(i % 5 == 0){
 try {
 sleep(5000);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 }
 }
 System.out.println("i=" + i);
 }
 }
}
java
【测试代码】
/**
 * 测试线程休眠
 */
public class TestSleep {
 public static void main(String[] args) {
 new MyThread().start();
 }
}

21.11 线程间通信

21.11.1 需求

要求通过线程输出1~100之间的所有整数。两个线程交替执行。

21.11.2 实现

java
【线程代码】
/**
 * 线程间通信
 */
class PrintNum implements Runnable{
 private int num = 1;

 /**
 * wait():让当前正在执行的线程处于等待状态,wait()会释放线程锁
 */
 `@Override`
 public void run() {
 while(true){
 synchronized (this) {
 //唤醒其他线程
 notify();
 //notifyAll();
 System.out.println(Thread.currentThread().getName() + " -- num:" + num++);
 if (num >= 100)
 break;
 try {
 //是当前线程处于等待状态
 //需要其他线程唤醒才能继续执行
 wait();
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 }
 }
 }
}
java
【测试】
/**
 * 要求通过线程输出1~100之间的所有整数。两个线程交替执行。
 */
public class TestWait {
 public static void main(String[] args) {
 PrintNum t = new PrintNum();

 Thread t1 = new Thread(t, "线程1");
 Thread t2 = new Thread(t, "线程2");

 t1.start();
 t2.start();
 }
}

21.11.3 常用方法

  • Wait():使当前线程挂起并释放Cpu资源及同步资源的等待。使其他线程可以访问共享资源并对其进行修改。当前线程队列的其他线程通过notify()或notifyAll()方法可以唤醒。唤醒后的线程需要重新获得监视器的所有权才能继续执行
  • Nofity():唤醒正在等待队列中等待同步资源的线程,等待队列中优先级较高的线程结束等待
  • nofityAll():唤醒在队列中等待同步资源的所有线程

21.11.4 说明

  • 方法只能用在同步方法或代码块中,否则将产生异常
  • 三个方法必须有锁对象才能进行调用,任意类型的对象都可以作为同步锁

21.11.5 Wait()

  • 当前线程中调用方法,可以通过对象名.wait()进行调用
  • Wait()会是当前线程进入到等待状态,直到另一个线程对象发出notify()为止
  • 调用的前提条件:当前线程必须具有对象的监视权(锁)
  • 调用此方法后,当前线程会释放对象的监视权,进入到等待状态
  • 当前线程被notify()唤醒后需要重新获得监视权,然后从中断处开始继续执行

21.11.6 Notify()/notifyAll()

  • 在当前线程中调用方法:对象名.notify()
  • 作用:唤醒等待监视权的一个(多个)线程
  • 前提条件:当前线程必须具有对象的监视权(锁)

21.12 Callable接口

21.12.1 概述

传统的线程实现方式有一个较大的局限性:无法直接返回线程的执行结果。为了解决这个问题,Java5中引入了Callable接口。他允许线程在执行结束后返回一个结果。并且可以抛出异常。

21.12.2 底层实现

  1. 接口
java
`@FunctionalInterface`
public interface Callable<V> {
 V call() throws Exception;
}
  1. 说明
  • V:类型占位符,表示call()方法的结果类型
  • Call():执行任务并返回结果,可能会抛出异常
  1. 示例
java
【创建线程】
/**
 * Callable方式实现线程
 */
public class TestCallable {
 public static void main(String[] args) {
 }
}

class CalThread implements Callable<Integer> {
 private int num1 = 0;
 private int num2 = 0;

 public CalThread(int num1, int num2) {
 this.num1 = num1;
 this.num2 = num2;
 }

 `@Override`
 public Integer call() throws Exception {
 return num1 + num2;
 }
}
java
【使用线程】
/**
 * Callable方式实现线程
 */
public class TestCallable {
 public static void main(String[] args) {
 //1、创建一个具有固定大小的线程池
 //Executors:提供构建线程池的相关方法
 ExecutorService service = Executors.newFixedThreadPool(2);

 //2、创建线程对象
 CalThread t1 = new CalThread(20, 30);
 CalThread t2 = new CalThread(44, 51);

 //3、提交任务并获取Future对象
 Future<Integer> future1 = service.submit(t1);
 Future<Integer> future2 = service.submit(t2);

 //4、获取执行结果
 try {
 //获取执行结果
 Integer result1 = future1.get();
 Integer result2 = future2.get();

 System.out.println("result1=" + result1);
 System.out.println("result2=" + result2);
 } catch (InterruptedException e) {
 throw new RuntimeException(e);
 } catch (ExecutionException e) {
 throw new RuntimeException(e);
 }
 finally {
 //5、关闭线程池
 service.shutdown();
 }
 }
}
  1. Future接口
  • 概述

Future接口表示异步计算结果。

  • 常用方法
  • Get():获取计算结果,如果计算未完成,则阻塞当前线程的执行直到计算完成
  • Get(long,TimeUnit):在指定时间内获取计算结果,如果超时则抛出异常
  • isDone():判断任务是否完成
  • cancel(Boolean):尝试取消任务
  1. 注意事项
  • 阻塞问题:Future.get()方法会阻塞线程,直到任务完成。如果任务时间较长,可能会导致主线程阻塞
  • 取消任务:通过Furture.cancel()可以取消任务的执行,但只有在任务尚未开始或允许中断才能取消
  1. Callable与Future的应用场景
  • 需要返回结果的多线程任务,如:计算密集型任务,数据库查询等
  • 需要处理异常的多线程任务,如:网络请求、文件操作等
  • 任务超时控制:通过Future.get()可以设置任务的超时时间,避免长时间等待
  1. callable和Runnable的区别
特征RunnableCallable
返回值无返回值有返回值
异常处理不能抛出受检异常可以抛出受检异常
线程池支持ExecutorService.execute()ExcutorService.submit()
使用场景简单异步任务具有返回结果和异常处理的任务

21.13 线程池

21.13.1 概述

线程的创建和销毁都要映射到操作系统。其代价是比较高的。为了避免频繁的创建、销毁线程。线程池就产生了。

21.13.2 优势

  1. 降低资源消耗

线程池通常会维护一些线程(corePoolSize决定),线程池中的线程被重复用来执行不同的任务。任务执行后不会进行销毁。避免了线程的重复创建和销毁,降低了资源的消耗

  1. 提高响应速度

线程池中维护一定数量的线程,这些线程将处于alive状态。不需要重新创建,可以直接进行使用。减少了任务的等待时间。

  1. 便于线程的管理

使用线程池可以对线程进行统一的分配、监控和优化。

21.13.3 实现理解

理解:

公司有正式员工,正式员工去完成正常任务,当任务过多忙不过来时,可以通过兼职方式来增加员工数量。当任务数量过多时,所有员工都忙不过来时可以拒绝任务。等到任务较少时会有员工空闲。当空闲人员达到一定数量时可以根据情况适当的解聘一些兼职人员。所有的操作都有主管来进行调度。

21.13.4 执行流程

21.13.5 创建线程池

  1. 继承关系

  1. 构造器
java
public ThreadPoolExecutor(int corePoolSize,
 int maximumPoolSize,
 long keepAliveTime,
 TimeUnit unit,
 BlockingQueue<Runnable> workQueue,
 ThreadFactory threadFactory,
 RejectedExecutionHandler handler)
  • corePoolSize(必选):核心线程数,即线程池中一直保持存活的线程数。即使核心线程处于空闲状态也会存活。除非设置了参数allowCoreThreadTimeOut为true后,核心线程空闲一定时间后也会回收
  • maximumPoolSize(必选):线程池中允许的最大线程数,当所有核心线程处于繁忙状态,且任务队列已满,线程池会临时新增线程。maximumPoolSize设置了线程的总上限数
  • keepAliveTime(必选):线程空闲超时时间,当非核心线程处于空闲状态一定时间后,该线程将被回收。如果设置了allowCore

ThreadTimeOut参数后,核心线程也会被回收

  • unit(必选):设置keepAliveTime参数的时间单位。取值包含:Days、Hours等
  • workQueue(必选):任务队列,采用阻塞队列实现。当核心线程全部繁忙时,后续由execute()方法提交的线程对象放在任务队列中等待处理
  • threadFactory(可选):线程工厂,执行线程池创建线程的方式
  • handler(可选):拒绝策略。当达到最大线程数且队列已满,后续提交的线程任务将被拒绝。该参与用于指定任务拒绝方式。
  1. 任务队列
  • SynchronousQueue:同步队列,内部没有任何容量的阻塞队列,任何一次插入操作的元素都要等待相对的删除、读取操作。否则进行插入操作的线程就要一直等待
  • LinkedBlockingQueue:无界队列,基于链表结构。使用无界队列后,当核心线程都繁忙,后续任务可以无限的加入到队列中。所以线程池中线程数不会超过核心线程数。这种队列可以提高线程的吞吐量。无界队列牺牲的是内存空间,可能会导致内存溢出
  • ArrayBlockingQueue:有界队列,基于数组实现。在线程池初始时,指定队列容量。后续无法调整。有界队列可以避免资源耗尽。
  1. 拒绝策略

拒绝策略是线程池的一个重要机制。当线程池的队列已满且无法创建新线程时,就要拒绝后面的其他任务。

  • AbortPolicy(默认):丢弃任务并抛出异常
  • CallerRunsPolicy:直接运行任务的run方法,但并不是通过线程池进行处理,而是交给任务的调度线程处理
  • DiscardPolicy:直接丢弃任务,不抛出任何异常
  • DiscardOldestPolicy:将当前线程处于等待队列头部的线程强行去除,然后在试图执行当前被拒绝的任务提交到线程池中
  1. SingleThreadExecutor(单线程线程池)

特点:

线程池中只有一个线程(核心线程)。线程执行完任务后立即回收。使用有界阻塞队列(容量未指定,默认容量Integer.MAX_VALUE)。

示例:

java
/**
 * 单线程线程池
 */
public class TestSinglePooled {
 public static void main(String[] args) {
 //1、创建线程池
 ExecutorService service = Executors.newSingleThreadExecutor();

 //2、创建线程任务对象
 Runnable task = new Runnable() {
 `@Override`
 public void run() {
 System.out.println(Thread.currentThread().getName()+"--->运行了.......");
 }
 };

 //3、将任务提交给线程池
 service.execute(task);
 }
}
  1. ScheduledThreadPool(定时线程池)

特点:

指定核心线程的数量,普通线程数量无限。线程执行完任务后立即回收,任务队列为延时阻塞队列。这是一个比较特殊的线程池,适用于执行定时或周期性任务。

示例:

java
/**
 * 测试定时线程池
 */
public class TestSchedulePool {
 public static void main(String[] args) {
 //1、创建线程池对象
 ScheduledExecutorService service = Executors.newScheduledThreadPool(5);

 //2、创建线程任务对象
 Runnable task = new Runnable() {
 `@Override`
 public void run() {
 System.out.println(Thread.currentThread().getName() + "--->运行了.......");
 }
 };

 //3、向线程池提交任务
 //任务延迟5后立即执行
 //参数1:待执行任务
 //参数2:时间长度
 //参数3:时间单位
 //service.schedule(task,5, TimeUnit.SECONDS);
 //周期性定时任务
 service.scheduleAtFixedRate(task, 5, 3, TimeUnit.SECONDS);
 }
}

| :--- |

  1. CachedThreadPool(缓存线程池)

特点:

没有核心线程数,普通线程数量为Integer.MAX_VALUE(可以理解为无限)。线程空闲60S后回收。任务队列使用SynchornousQueue。可以理解为无容量同步队列。适用于大量任务但耗时较低的场景。

示例:

java
/**
 * 测试CachedThreadPool
 */
public class TestCachePool {
 public static void main(String[] args) {
 //1、创建线程池对象
 ExecutorService service = Executors.newCachedThreadPool();

 //2、创建线程任务对象
 Runnable task = new Runnable() {
 `@Override`
 public void run() {
 System.out.println(Thread.currentThread().getName() + "--->运行了.......");
 }
 };

 //3、向线程池提交任务
 service.execute(task);
 }
}
  1. FixedThread(固定大小线程池)

特点:

具有固定数量的核心线程。

示例:

java
/**
 * 测试FixThreadPool
 */
public class TestFixedPool {
 public static void main(String[] args) {
 //1、创建线程池对象
 ExecutorService service = Executors.newFixedThreadPool(5);

 //2、创建线程任务对象
 Runnable task = new Runnable() {
 `@Override`
 public void run() {
 System.out.println(Thread.currentThread().getName() + "--->运行了.......");
 }
 };

 //3、向线程池提交任务
 service.execute(task);
 }
}

21.14 ThreadLocal

21.14.1 概述

ThreadLocal为线程提供本地变量。他提供了线程内部的独立变量。即所谓的线程独立的“变量副本”。

每个线程都有自己独立的变量。通过TheadLocal对象提供的方法可以访问独立变量。

21.14.2 作用

ThreadLocal用于解决线程并发是访问共享变量的问题。主要实现了数据隔离

21.14.3 实现原理

ThreadLocal的内部维护了一个Map对象,Map中的元素值都是键值对。其中键(Key)就是当前线程,Value就是当前线程需要存储的数据。

TheadLocal中的Map不是直接的HashMap,而是使用的ThreadLocal Map(他是静态内部类)。通过Map来存储线程数据。

21.14.4 示例

java
public class TestThreadLocal {
 public static void main(String[] args) throws InterruptedException {
 //1、创建ThreadLocal对象
 ThreadLocal<Long> threadLocal = new ThreadLocal<>();

 long time = System.currentTimeMillis();
 System.out.println("time=" + time);

 //2、添加线程数据
 threadLocal.set(time);

 //让当前线程休眠2秒
 Thread.sleep(2000);

 //3、读取线程数据
 System.out.println("读取值为:" + threadLocal.get());

 //4、移除线程数据
 threadLocal.remove();
 System.out.println("移除后的数据:" + threadLocal.get());
 }
}

21.14.5 使用场景

  1. 解决线程安全性

ThreadLocal为每个线程创建一个副本,每个线程都拥有自己的数据副本,修改数据时不会影响到其他线程,从而确保了线程安全性

  1. 代替参数的数据传递

在控制层接收来自前端的参数时,可以通过ThreadLocal来替代参数的显式传递,而直接将参数添加到ThreadLocal中。

  1. 存储全局用户信息

使用ThreadLocal替代Session,是每个线程都拥有独立的Session对象。

21.15 练习

  1. 编写代码模拟龟兔赛跑。兔子速度较快,但中途会休息一定时间长度。乌龟速度较慢
  2. 体育场输出CAB球赛入场券,总计有1000张,现开设3个窗口进行销售。编写代码模拟销售过程

22 集合框架

22.1 数组

  1. 缺点
  • 长度固定
  • 不便于元素的操作,如:插入元素、移除元素等

22.2 集合框架

Java中为了满足不同场景的需求,提供了一组集合类。集合类内提供了对于元素操作的相关方法。

(图1:Collection集合)

(图2:Map集合)

22.3 List(列表)

22.3.1 概述

List列表也称为序列,列表中的元素是有序的,可以通过索引对列表中元素进行精确访问。列表中的元素是允许重复的。

22.3.2 常用方法

  • Add():将新元素添加到列表的末尾
  • Add(int,T):将元素插入到列表的指定位置
  • isEmpty():返回列表是否为空
  • Contains():返回列表中是否包含指定值的元素
  • Get():返回指定索引位置的元素值
  • Set():修改指定索引位置的元素值
  • Size():返回列表中元素的个数
  • indexOf():指定值的元素在列表中首次出现的索引位置
  • lastIndexOf():指定值的元素在列表中最后一次出现的索引位置
  • iterator():返回此列表的迭代器
  • remove():移除指定值(或索引位置) 的元素
  • toArray():将List对象转为数组对象

22.3.3 ArrayList

  1. 概述

ArrayList是List接口的数组实现。ArrayList集合中允许存在Null值。该类是非线程安全的。

  1. 对象创建
java
//1、对象创建
//调用无参构造,默认初始容量为10
List<Integer> list1 = new ArrayList<>();
//调用带参构造,自定义集合的初始容量
List<Integer> list2 = new ArrayList<>(20);
  1. 底层实现
java
public class ArrayList<E> extends AbstractList<E>
 implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 /**
 * Default initial capacity.
 * 默认初始容量
 */
 private static final int DEFAULT_CAPACITY = 10;

 /**
 * Shared empty array instance used for empty instances.
 * 用于赋值空列表对象的组对象
 */
 private static final Object[] EMPTY_ELEMENTDATA = {};

 /**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

 /**
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
 transient Object[] elementData; // non-private to simplify nested class access

 /**
 * The size of the ArrayList (the number of elements it contains).
 *
 * `@serial`
 */
 private int size;

 /**
 * Constructs an empty list with the specified initial capacity.
 *
 * `@param` initialCapacity the initial capacity of the list
 * `@throws` IllegalArgumentException if the specified initial capacity
 * is negative
 */
 public ArrayList(int initialCapacity) {
 if (initialCapacity > 0) {
 this.elementData = new Object[initialCapacity];
 } else if (initialCapacity == 0) {
 this.elementData = EMPTY_ELEMENTDATA;
 } else {
 throw new IllegalArgumentException("Illegal Capacity: "+
 initialCapacity);
 }
 }

 /**
 * Constructs an empty list with an initial capacity of ten.
 */
 public ArrayList() {
 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
 }
}
  1. 常用方法
java
//2、常用方法
//a、add():添加(插入)一个新元素到列表中
list1.add(20);
list1.add(43);
list1.add(0, 78);

//b、isEmpty():返回当前列表是否为空列表
System.out.println(list1.isEmpty());

//c、contains():返回集合对象是否包含指定值的元素
System.out.println(list1.contains(43));

//d、get():返回指定索引位置的元素值
System.out.println(list1.get(1));

//e、set():修改指定索引位置的元素值
list1.set(1, -96);

//f、size():返回当前列表中元素的个数
System.out.println(list1.size());

//g、indexOf():返回元素在集合中首次出现的索引位置
System.out.println(list1.indexOf(-96));
System.out.println(list1.indexOf(120));

//h、remove():移除指定索引位置的元素值
list1.remove(1);

//i、clear():清除集合中所有元素
list1.clear();
System.out.println(list1);
  1. 元素遍历
java
//3、元素遍历
//a、for循环遍历元素
for(int i = 0; i < list1.size(); i++)
 System.out.println(list1.get(i));

//b、增强for循环
for(int i : list1)
 System.out.println(i);

//c、迭代器方式遍历
Iterator<Integer> itr = list1.iterator();
while(itr.hasNext()){
 System.out.println(itr.next());
}
  1. 扩容机制
java
//添加新元素
public boolean add(E e) {
 modCount++;
 add(e, elementData, size);
 return true;
}
java
//添加元素到elementData
private void add(E e, Object[] elementData, int s) {
 if (s == elementData.length)
 elementData = grow();
 elementData[s] = e;
 size = s + 1;
}
java
//执行扩容操作
private Object[] grow() {
 return grow(size + 1);
}
java
private Object[] grow(int minCapacity) {
 int oldCapacity = elementData.length;
 if (oldCapacity > 0 |


### 22.3.4 Vector
1. 概述

Vector的底层是基于数组进行实现。Vector是线程安全的。

1. 对象创建

```java
//1、创建对象
List<Integer> list1 = new Vector<>();
  1. 常用方法

  1. 扩容机制
java
public synchronized boolean add(E e) {
 modCount++;
 add(e, elementData, elementCount);
 return true;
}
java
private void add(E e, Object[] elementData, int s) {
 if (s == elementData.length)
 elementData = grow();
 elementData[s] = e;
 elementCount = s + 1;
}
java
private Object[] grow() {
 return grow(elementCount + 1);
}
java
//Jdk1.8源码
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
 capacityIncrement : oldCapacity);

22.3.5 LinkedList

  1. 概述

LinkedList是List接口的双向链表实现有序集合。LinkedList允许以队列、堆栈的方式进行操作。

  1. 对象创建
java
//1、对象创建
LinkedList<Integer> list = new LinkedList<>();
  1. 底层实现
java
public class LinkedList<E>
 extends AbstractSequentialList<E>
 implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
 transient int size = 0;

 /**
 * Pointer to first node.
 * 指向第一个节点的指针
 */
 transient Node<E> first;

 /**
 * Pointer to last node.
 * 指向最后一个节点的指针
 */
 transient Node<E> last;
}
java
【Node】
private static class Node<E> {
 E item; //存储节点数据
 Node<E> next; //存储前一个(前驱)节点
 Node<E> prev; //存储后一个(后继)节点

 Node(Node<E> prev, E element, Node<E> next) {
 this.item = element;
 this.next = next;
 this.prev = prev;
 }
}
  1. 专属方法
java
//以堆栈方式操作
//a、push():将新元素压入到堆栈
list.push(72);

//b、pop():返回并从列表中移除列表的第一个元素
int i = list.pop();
System.out.println(i);

//c、poll():返回并从列表中移除列表的第一个元素(队列方式)
i = list.poll();
System.out.println(i);

//d、peek():返回并从列表中列表的第一个元素但不移除元素
i = list.peek();
System.out.println(i);
System.out.println(list);
  1. 元素遍历
java
//3、遍历元素
//for循环遍历
for(int l = 0; l < list.size(); l++)
 System.out.println(l + "\t" + list.get(l));

//增强for循环
for(int t : list)
 System.out.println(t);

//迭代器
Iterator<Integer> itrs = list.iterator();
while(itrs.hasNext()){
 System.out.println(itrs.next());
}
  1. 优势
  • 插入、移除元素具有更高的效率

22.4 Set

22.4.1 概述

Set是一个不包含重复元素的集合对象,Set中最多值允许包含一个null值元素。

22.4.2 常用方法

  • Add():添加元素到set集合
  • isEmpty():返回当前set集合是否为空
  • contains():返回set集合中是否包含指定值的元素
  • remove():移除指定元素
  • clear():清除set集合中所有元素
  • iterator():返回此set集合的迭代器
  • toArray():将set对象转为数组对象

22.4.3 HashSet

  1. 概述

HashSet的底层是基于HashMap进行实现的,HashSet是一个无序不重复的集合。HashSet中允许使用null元素。

  1. 创建对象
java
//1、创建对象
//调用无参构造,默认初始容量为16,加载因子为0.75
Set<Integer> set1 = new HashSet<>();
//调用带参构造,指定初始容量,默认加载因子为0.75
Set<Integer> set2 = new HashSet<>(20);
  1. 底层实现
java
public class HashSet<E>
 extends AbstractSet<E>
 implements Set<E>, Cloneable, java.io.Serializable {
 private transient HashMap<E,Object> map;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

 /**
 * Constructs a new, empty set; the backing {`@code` HashMap} instance has
 * default initial capacity (16) and load factor (0.75).
 */
 public HashSet() {
 map = new HashMap<>();
 }
}
  1. 常用方法

  1. 元素的遍历
java
//3、遍历集合中元素
//a、for循环
//set没有提供get()方法,所以无法通过索引下标(for循环)进行遍历
//for(int i = 0;i<set1.size();i++)

//b、增强for循环
for(int i : set1)
 System.out.println(i);

//c、迭代器
Iterator<Integer> itrs = set1.iterator();
while(itrs.hasNext())
 System.out.println(itrs.next());
  1. 特点
  • 无序、不重复
  • 可以包含null值

22.4.4 TreeSet

  1. 概述

TreeSet的底层是基于TreeMap的实现。使用自然排序顺序对元素进行排序。

  1. 对象创建
java
//调用无参构造,使用自然排序
Set<Integer> set1 = new TreeSet<>();

//调用带参构造,自定义比较方式
Set<Integer> set2 = new TreeSet<>(new Comparator<Integer>() {
 `@Override`
 public int compare(Integer o1, Integer o2) {
 return 0;
 }
});
  1. 底层实现
java
public class TreeSet<E> extends AbstractSet<E>
 implements NavigableSet<E>, Cloneable, java.io.Serializable {
 /**
 * The backing map.
 * NavigatableMap是一个接口,所以无法进行实例化
 * 实例化的是TreeMap
 */
 private transient NavigableMap<E,Object> m;

 // Dummy value to associate with an Object in the backing Map
 private static final Object PRESENT = new Object();

 /**
 * Constructs a new, empty tree set, sorted according to the
 * 按照自然排序顺序构建一个TreeSet
 */
 public TreeSet() {
 this(new TreeMap<>());
 }
}
  1. 常用方法

参考HashSet

  1. 特点
  • 元素按照自然排序顺序进行排序

22.4.5 LinkedHashSet

  1. 概述

LinkedHashSet的底层是基于哈希表+双向链表实现的。LinkedHashSet是一个有序列表。

  1. 创建对象
java
//调用无参构造,默认初始容量为16,加载因子为0.75
Set<Integer> set1 = new LinkedHashSet<>();
//调用带参构造,自定义初始容量为16,加载因子为0.75
Set<Integer> set2 = new LinkedHashSet<>(20);
  1. 底层实现
java
public class LinkedHashSet<E>
 extends HashSet<E>
 implements Set<E>, Cloneable, java.io.Serializable {

 /**
 * Constructs a new, empty linked hash set with the default initial
 * capacity (16) and load factor (0.75).
 */
 public LinkedHashSet() {
 super(16, .75f, true);
 }
}
  1. 常用方法

参考HashSet

  1. 特点
  • 遍历顺序与添加顺序保持一致

22.5 Map

22.5.1 概述

Map是键值对映射的集合。Map中不能包含重复的键。

22.5.2 常用方法

  • Put():将一个键值对添加到map集合中
  • isEmpty():返回是否为空Map集合
  • size():返回键值对映射的个数
  • containsKey():返回是否包含指定值的key
  • containsValue():返回是否包含指定值的value
  • get():返回key对应的映射值
  • keyset():返回key的set 集合
  • values():返回values的集合
  • entrySet():返回映射的集合
  • remove():按照key移除键值对映射

22.5.3 HashMap

  1. 概述

HashMap的底层是基于哈希表(数组+链表)的方式进行实现的,HashM ap中的key是无序不重复的。HashMap中的映射允许使用null值的key和value。当存储容量达到填充因子*Map长度值时将执行扩容操作。扩容后新容量是原容量的2倍。

  1. 对象创建
java
//1、创建对象
//调用空构造函数,默认初始容量为16,加载因子:0.75
Map<Integer,String> map1 = new HashMap<>();
//调用带参构造函数,指定初始容量,加载因子默认为0.75
Map<Integer,String> map2 = new HashMap<>(20);
  1. 底层实现
java
public class HashMap<K,V> extends AbstractMap<K,V>
 implements Map<K,V>, Cloneable, Serializable {
 /**
 * The default initial capacity - MUST be a power of two.
 * 默认初始容量,必须是2的n次幂
 */
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

 /**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30
 * 最大存储容量.
 */
 static final int MAXIMUM_CAPACITY = 1 << 30;

 /**
 * The load factor used when none specified in constructor.
 * 填充因子(警戒线)
 */
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

 /**
 * 树化的基本阈值
 */
 static final int TREEIFY_THRESHOLD = 8;

 /**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
 static final int UNTREEIFY_THRESHOLD = 6;

 /**
 * 树化的最小阈值
 */
 static final int MIN_TREEIFY_CAPACITY = 64;

 /**
 * Basic hash bin node, used for most entries. (See below for
 * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
 */
 static class Node<K,V> implements Map.Entry<K,V> {
 final int hash;
 final K key;
 V value;
 Node<K,V> next;

 Node(int hash, K key, V value, Node<K,V> next) {
 this.hash = hash;
 this.key = key;
 this.value = value;
 this.next = next;
 }
 }

 /**
 * The table, initialized on first use, and resized as
 * necessary. When allocated, length is always a power of two.
 * (We also tolerate length zero in some operations to allow
 * bootstrapping mechanics that are currently not needed.)
 */
 transient Node<K,V>[] table;
}
  1. 常用方法
java
//2、常用方法
//a、put():将一个键值对添加到map集合
map1.put(2, "Tom");
map1.put(7, "Jerry");
map1.put(1, "李雷");

//b、isEmpty():返回当前map对象是否为空
System.out.println(map1.isEmpty());

//c、size():返回map集合中映射的个数
System.out.println(map1.size());

//d、containsKey():返回集合中是否包含指定值的key
System.out.println(map1.containsKey(87));

//e、get():返回指定key的映射值
System.out.println(map1.get(7));

//f、entrySet():返回键值对映射的set集合
Set<Map.Entry<Integer,String>> entrys = map1.entrySet();

//g、keySet():返回key的set集合
Set<Integer> keys = map1.keySet();
System.out.println(keys);

//h、values():返回map集合中值的collection集合
Collection<String> values = map1.values();
System.out.println(values);
System.out.println(map1);
  1. 元素遍历
java
//3、元素遍历
//a、遍历键值对:可以通过增强for循环、Iterator迭代器进行遍历
Set<Map.Entry<Integer,String>> entrys = map1.entrySet();
for(Map.Entry<Integer, String> entry : entrys)
 System.out.println(entry.getKey() + "\t" + entry.getValue());

//b、遍历key:可以通过增强for循环、Iterator迭代器进行遍历
Set<Integer> keys = map1.keySet();
for(Integer key : keys)
 System.out.println(key + "\t" + map1.get(key));

//c、遍历value:可以通过增强for循环、Iterator迭代器进行遍历
Collection<String> values = map1.values();
for(String v : values)
 System.out.println(v);
  1. 扩容
java
//将映射添加到Map集合
public V put(K key, V value) {
 return putVal(hash(key), key, value, false, true);
}
java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 boolean evict) {
 //声明了Node节点
 Node<K,V>[] tab;
 Node<K,V> p;
 //n--保存tab的长度
 int n, i;
 //将table赋值给tab并验证是否为null
 //将tab的length赋值给n,然后验证n是否为0
 if ((tab = table) == null |
```java
final Node<K,V>[] resize() {
 int oldThr = threshold;
 int newCap, newThr = 0;
 if (oldCap > 0) {
 if (oldCap >= MAXIMUM_CAPACITY) {
 threshold = Integer.MAX_VALUE;
 return oldTab;
 }
 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 oldCap >= DEFAULT_INITIAL_CAPACITY)
 newThr = oldThr << 1; // double threshold
 }

 return newTab;
}
  1. HashMap的底层实现
  • Jdk1.7前使用数组+链表方式进行实现,Jdk1.8开始使用数组+链表+红黑树方式进行实现
  • 当链表的节点数等于8且数组容量大于64后才转为红黑树
  1. 特点
  • 数据是键值对的集合
  • 键和值可以为null,键不能重复
  • 键是无序的,顺序不是恒久不变的
  • 可以通过键获取对应的映射值

22.5.4 HashTable

  1. 概述

HashTable的底层是基于哈希表(数组+链表)进行实现的,HashTable中key不能为null,默认初始容量是11,默认加载因子为0.75。HashTable是线程安全的。

  1. 初识容量
java
public class Hashtable<K,V>
 extends Dictionary<K,V>
 implements Map<K,V>, Cloneable, java.io.Serializable {

 /**
 * The hash table data.
 */
 private transient Entry<?,?>[] table;

 /**
 * The load factor for the hashtable.
 *
 * `@serial`
 */
 private float loadFactor;

 /**
 * Constructs a new, empty hashtable with a default initial capacity (11)
 * and load factor (0.75).
 */
 public Hashtable() {
 this(11, 0.75f);
 }
}
  • 初识容量:11
  1. 扩容机制
java
int newCapacity = (oldCapacity << 1) + 1;
  • 新容量 = 原容量*2 +1

22.5.5 TreeMap

  1. 概述

TreeMap是基于红黑树进行实现的,TreeMap根据键的自然排序顺序进行排序。

  1. 常用方法

参考HashMap

  1. 特点
  • 根据键的自然排序顺序进行排序,可以自定义排序方式

22.5.6 LinkedHashMap

  1. 概述

LinkedHashMap的底层是基于哈希表+链表的实现。LinkedHashMap元素的遍历顺序与添加顺序保持一致。

  1. 底层实现
java
/**
 * Constructs an empty insertion-ordered {`@code` LinkedHashMap} instance
 * with the default initial capacity (16) and load factor (0.75).
 */
public LinkedHashMap() {
 super();
 accessOrder = false;
}
java
/**
 * Constructs an empty {`@code` HashMap} with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
 this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  1. 特点
  • 遍历顺序与插入顺序保持一致

22.6 总结

  • 凡是带有Tree的没有初始容量,没有扩容
  • 凡是List初始容量基本都是10,Map初始容量基本都是16

22.7 Collections

  1. 概述

Collections是一个工具类,类中提供了基于集合对象元素操作的静态方法。

  1. 常用方法
  • binarySearch():使用二分查找方式查找集合中元素
  • Copy():复制集合中所有元素到另一个列表
  • Fill():使用指定值填充集合中所有元素
  • Reverse():对集合中所有元素进行翻转
  • Sort():对集合中所有元素按照自然顺序进行排序
  • synchornizedXxx():使集合对象支持同步

22.8 问题

  1. 简述ArrayList、Vector及LinkedList的区别。
  • 底层实现:ArrayList、Vector的底层是基于数组实现的;Linked List的底层是基于链表实现的
  • 线程安全性:Vector是线程安全的;ArrayList、LinkedList是非线程安全的
  • 初识容量:ArrayList、Vector的初始容量为10;LinkedList没有初始容量
  • 扩容机制:ArrayList的扩容机制为新容量为原容量的1.5倍;Vector的新容量为原容量的2倍;LinkedList不需要扩容
  • 特点:ArrayList、Vector顺序访问具有更高的效率;LinkedList插入移除元素效率更高
  1. 简述List、Set和Map之间的区别
  2. 简述HashMap与HashTable的区别
  • HashMap继承于AbstractMap;HashTable继承于Dictionary
  • HashMap的默认初始容量为16;HashTable默认初始容量为11
  • HashMap的默认扩容为原容量2倍;HashTable默认与扩容为原容量2倍+1
  • HashMap是非线程安全的;HashTable是线程安全的
  • HashMap在Jdk1.8前底层使用数组+链表实现,Jdk1.8开始采用数组+链表+红黑树进行实现;HashTable的底层使用数组+链表进行实现
  1. 简述Collections和Collection的区别

23 泛型

23.1 概述

泛型就是广泛化的类型,泛型相当于C++中的模版,但有所不同。反向其实就是将类型作为参数进行传递。

23.2 泛型类

  1. 语法
java
[修饰符] class 类名<T>{
 [类成员]
}


1. 说明
+ T称为类型占位符。可以是任意的一个大写字母
+ 可以有多个占位符,各占位符之间用”,”进行分隔
2. 示例

```java
【泛型类】
/**
 * 实现工具类,多数组的操作进行一个封装
 */
public class ArrayUtil<T> {
 //定义数组来存储数据
 private Object[] array;
 //定义变量来保存当前插入元素的索引位置
 private int index = 0;

 //构造器,实例化数组对象对象
 public ArrayUtil() {
 this(10);
 }

 public ArrayUtil(int initCapacity) {
 if(initCapacity < 0 || initCapacity >= Integer.MAX_VALUE)
 throw new RuntimeException("错误的初始容量");
 array = new Object[initCapacity];
 }

 /**
 * 添加新元素到数组
 */
 public void add(T value){
 array[index++] = value;
 }

 /**
 * 返回指定索引位置的元素值
 */
 public T get(int index){
 return (T)array[index];
 }
}
java
【测试】
/**
 * 测试自定义泛型类
 */
public class TestUtil2 {
 public static void main(String[] args) {
 //注意,使用泛型类时,<>内必须使用包装类型或引用类型
 ArrayUtil<Integer> util1 = new ArrayUtil<>();
 util1.add(2);

 int i = util1.get(0);
 System.out.println(i);

 ArrayUtil<String> uitl2 = new ArrayUtil<>();
 uitl2.add("Hello");
 }
}
  1. 实现原理

编译器在对代码进行编译时,用具体的Java类型来替换占位符。

23.3 泛型方法

  1. 语法
java
[修饰符] <占位符> 返回值类型([参数列表]){
 [方法体]
}
  1. 示例
java
/**
 * 泛型方法
 */
public class TestGenerateMethod {
 public static void main(String[] args) {
 show("abc");
 show(123);
 show(true);
 }

 /**
 * 泛型方法
 * `@param` t
 * `@param` <T>
 */
 private static <T> void show(T t) {
 System.out.println(t);
 }
}
  1. 注意事项

占位符只能定义在修饰符和返回值类型的中间

24 哈希表

24.1 需求1

24.1.1 需求

现有一个数值序列,数值的内容为:[2,8,9,0,3,1]。编写代码实现从键盘上输入一个数,可以快速验证该数值是否存在。

24.1.2 实现

java
【思路1】
private static int[] array = {2,8,9,0,3,1};
/**
 * 验证key在array中是否存在
 * 实现思路:通过遍历的方式注意进行比较(线性探测法)
 */
private static boolean find(int key) {
 for(int t : array){
 if(t == key)
 return true;
 }

 return false;
}
java
【思路2】
private static int[] array2 = {0,1,2,3,-1,-1,-1,-1,8,9};
/**
 * 验证key在array中是否存在
 * 实现思想:将数值与数组的索引下标进行一对一映射。查询时直接将关键字作为数组下标进行使用
 */
private static boolean find2(int key) {
 return array2[key] != -1;
}

24.2 需求2

24.2.1 需求

存储N名学生的信息,要求可以按照学号实现学生信息的快速查找。

学号姓名
0Tom
1Jerry
2Mike
3Rose
4李雷

24.2.2 实现思路

定义一个具有5个长度的数组来存储学生的信息,我们发现学号与数组的索引下标相对应,所以索引下标也就是学生的学号。

24.2.3 实现

java
【Student类】
class Student{
 private int no;
 private String name;

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

 public int getNo() {
 return no;
 }

 public void setNo(int no) {
 this.no = no;
 }

 public String getName() {
 return name;
 }

 public void setName(String name) {
 this.name = name;
 }
}
java
【测试类】
/**
 * 存储N名学生的信息,要求可以按照学号实现学生信息的快速查找
 */
public class Demo02 {
 private static Student[] students = {
 new Student(0,"Tom"),
 new Student(1,"Jerry"),
 new Student(2,"Mike"),
 new Student(3,"Rose"),
 new Student(4,"李雷")
 };

 public static void main(String[] args) {
 int key = 8;
 Student result = find(key);
 System.out.println(result);
 }

 private static Student find(int key) {
 if(key < 0 || key >= students.length)
 return null;
 return students[key];
 }
}

24.2.4 散列表

  1. 散列表

示例中存储数据的数组就是散列表,也称为哈希表。散列表中关键字(Key)和数组的下标(数据存储的地址)之间建立一种映射关系。

  1. 散列函数

我们可以通过函数计算出数据存储的地址,这个函数称为散列函数(哈希函数)

  • K:查询关键字
  • H(K):散列函数(也称为哈希函数),可以计算出映射到散列表中的地址。

24.3 需求3

24.3.1 需求

有一个N个长度的整数序列:[101,102,103,104,105,106],编写代码实现快速查找功能。

24.3.2 实现思路

定义一个具有106个长度的数组,然后将数据与数组的索引下标建立一个映射关系。查询时根据数组索引下标进行直接查询数据。

24.3.3 缺点

  • 数值序列中只有6个,定义一个具有106个长度的存储空间,会造成较大的空间浪费

24.3.4 升级方案

实际上只需要6个存储空间。所以我们定义具有6个容量的数组对象。然后修改散列函数:

H(K) = K-101

修改后的散列函数计算得到的地址就是从0开始的。这种计算方式我们称为直接地址计算法。直接地址计算法适用于计算连续的地址。

24.4 需求4

24.4.1 需求

有一个N个长度的整数序列:[78,63,102,871,45,532],编写代码实现快速查找功能。

24.4.2 问题

序列分布比较散,如果使用直接地址定位法会存在很多空间浪费的问题。

24.4.3 实现

A除以B得到的余数一定小于B的值,如果把元素个数记作P。那么可以得到公式:

H(K) = Key % P

这种计算方式称为”除留余数法”。

24.4.4 哈希冲突

一般情况下使用这种计算方式得到的结果会存在一定概率的重复值。这种重复值会造成地址的冲突。即通常所说的哈希冲突(或哈希碰撞)。

哈希冲突将导致性能的下降。但是在存储上可以有效避免空间浪费。

两个关键字经过散列函数计算后得到相同的地址,结果导致映射位置到了同一个位置,就是我们所说的冲突,也称为碰撞。地址相同的两个关键字称为同义词。

24.4.5 填充因子

冲突会导致散列表的性能下降。冲突越多散列表的性能越差。如果希望散列表具有较好的性能,那么就应该尽量避免冲突。

除了冲突之外,影响散列表性能的另一个指标就是空间的利用率。一般也称为填充因子。填充因子一般用α进行表示。

α(填充因子) = n(表中元素个数) / m(表长)
α = 6 / 8 = 0.75

填充因子越大,因为表填充的越满,空闲位置越少。也就说明冲突的可能性越大。填充因子越小,空闲位置越多,冲突也越小。空间利用率也越小。

  • 填充因子越大,冲突的可能性越大,但空间利用率也越高
  • 填充因子越小,冲突的可能性越小,但空间利用率也越低

24.4.6 总结

  1. 直接地址计算法:适用于连续地址

公式:

H(Key) = Key

H(Key) = Key ±n

使用场景:

简单线性映射,不会出现映射冲突。只适用于分布连续的情况下使用

缺点:

对于大量的分散的数据会造成空间的浪费。

  1. 除留余数法:适用于分散数据的地址

公式:

markdown
//p一般是小于表长的最大质数
H(Key) = Key % p

使用场景:

将分散的数据映射到连续的空间中。

缺点:

可能会出现冲突(碰撞)。

解决:

可以通过填充因子处理冲突问题。

24.5 处理冲突

24.5.1 开放地址法

  1. 数据

  2. 计算表长

  3. 存储数据

通过计算公式

除留余数法 + 线性探测法

通过关键字除表长得到元素的存储地址,如果空间内已经存在数据,则继续向后进行查找,直到找到空余空间或表尾。查找到表尾没有找到空余空间,则从表头开始进行查找。

  1. 元素查找

查找元素时,首先计算出元素的地址,然后直接定位到对应的地址,如果空间内存储的是空标志位,则表示该关键字不存在。否则,对关键字与存储空间内的值进行比对。相等则证明元素存在,否则继续向后进行查找。直到比对相等或遇到空标志位为止。

  1. 删除元素

删除元素时,不能直接将元素的存储空间设置为空标志位,因为这样会导致其他数据查询时出现地址截断的情况(遇到空标志则不向后进行查询,而事实上数据是存在的)。删除数据时应该用删除标志进行填充(通常用序列中一定不会出现的数字进行表示,如:-1)。

24.5.2 拉链法

数据存储:

元素的查找:

查找元素时,首先通过散列函数计算除存储地址,然后到数组中进行查找,如果数组内存储的是空指针(通常用^表示),则表示该元素不存在。否则,根据存储的链表地址对链表进行逐一比较。如果找到则包含数据,否则则说明不包含数据。

删除元素:

25 Jdk新特性

25.1 Lambda表达式

25.1.1 需求

在类中以Runnable接口的方式实现线程功能。

25.1.2 实现

java
//Runnable是接口,无法对接口直接进行实例化
//所以这里使用的是匿名内部类方式进行实现
new Thread(new Runnable() {
 `@Override`
 public void run() {
 System.out.println("线程被执行了........");
 }
}).start();

| :--- |

25.1.3 代码分析

  • Runnable作为Thread类构造器的参数
  • Runnable是一个接口,在代码中为了省去实现类,所以实现使用了匿名内部类实现了接口的run()方法
  • 在匿名内部类中实现了run()方法

25.1.4 思考

能不能把代码更加简化?

java
new Thread(()->{
 System.out.println("线程被执行了.......");
}).start();
//常规写法
//new Thread(()->{System.out.println("线程被执行了.......");}).start();

| :--- |

25.1.5 概述

Lambda表达式的本质其实就是一个匿名函数,也可以理解为代码段的方式进行参数传递。

Lambda表达式简化了匿名内部类的使用,使得语法更加简单。

25.1.6 语法

Lambda表达式不关心方法名及返回值类型,他只需要关注参数列表及方法体。

java
//简化main方法
(String[] args)->{
 [方法体]
}
java
([参数列表])->{方法体}

25.1.7 说明

  • 参数列表:方法体所需要用到的参数
  • ->:分隔方法体和参数列表。
  • {}:方法体

25.1.8 无参方法

java
【Dao接口】
/**
 * Dao接口--接口中包含一个无参方法
 */
public interface UserDao {
 void add();
}
java
【Service类】
/**
 * 服务类
 */
public class UserService {
 //UserDao是一个接口
 public void add(UserDao dao){
 //调用了UserDao对象的add()方法
 dao.add();
 }
}
java
【实现】
/**
 * 测试Lambda表达式
 */
public class TestLambda {
 public static void main(String[] args) {
 UserService service = new UserService();

 //1、匿名内部类方式
 service.add(new UserDao() {
 `@Override`
 public void add() {
 System.out.println("UserDao.add() is do..........");
 }
 });

 //2、Lambda表达式方式
 service.add(()->{
 System.out.println("UserDao.add()方法被执行了......");
 });
 }
}

25.1.9 带参方法

java
【Dao接口】
/**
 * 接口
 */
public interface UserDao {
 //按Id删除用户信息
 void del(int id);
}
java
【Service类】
public class UserService {
 //调用Dao对象的方法
 public void del(int id, UserDao dao){
 dao.del(id);
 }
}
java
【实现】
/**
 * 测试带参Lambda表达式
 */
public class TestLambda {
 public static void main(String[] args) {
 UserService service = new UserService();

 //1、匿名内部类方式实现
 service.del(1, new UserDao() {
 `@Override`
 public void del(int id) {
 System.out.println("您正在准备删除Id为"+ id +"的数据....");
 }
 });

 //2、Lambda表达式方式
 //service中的方法的第二个参数为接口类型参数(且没有任何实现类)
 service.del(2,(int id)->{
 System.out.println("您正在准备删除Id为"+ id +"的数据....");
 });
 }
}

25.1.10 带返回值方法

java
【Dao接口】
/**
 * 接口--带返回值方法
 */
public interface UserDao {
 //查询指定性别的用户数量
 int findByGender(boolean gender);
}
java
【Service类】
public class UserService {
 //调用了Dao对象的方法
 public void findByGender(boolean gender, UserDao dao){
 //调用dao对象的方法
 int total = dao.findByGender(gender);
 System.out.println("total="+ total);
 }
}
java
【实现】
/**
 * 测试带返回值函数
 */
public class TestLambda {
 public static void main(String[] args) {
 UserService service = new UserService();

 //1、匿名内部类方式
 service.findByGender(true, new UserDao() {
 `@Override`
 public int findByGender(boolean gender) {
 return 20;
 }
 });

 //2、Lambda表达式方式
 service.findByGender(false,(boolean gender)->{
 return 18;
 });
 }
}

25.1.11 注意事项

  • Lambda表达式只对接口有效
  • 接口中有且只有一个抽象方法

25.1.12 @FunctionalInterface(注解)

  1. 示例
java
//注解:一种标记
`@FunctionalInterface`
public interface UserDao {
 void add();
}
  1. 说明

用于标注接口,表示当前接口是一个功能性接口。

功能性接口中必须有一个抽象方法,通过该注解可以保证接口定义的正确性(接口是否满足Lambda表达式的要求)。

25.1.13 简化方式

  1. 简化规则
  • 可以省略()内的参数类型
  • 如果()内仅有一个参数则可以省略()
  • 如果{}仅有一条语句,则可以省略{}、return语句及分号
  1. 示例

省略参数类型:

java
`@FunctionalInterfacepublic` interface Cal {
 //方法有2个参数
 int add(int num1, int num2);
}
/**
 * 省略参数类型
 */public class TestCal {
 public static void main(String[] args) {
 //1、定义Lambda表达式
 //省略了参数的类型
 Cal cal = (num1,num2)->{
 return num1+num2;
 };

 int r = cal.add(2,8);
 System.out.println("r=" + r);
 }
}

省略()的情况:

java

`@FunctionalInterfacepublic` interface Show {
 //方法带有1个参数
 void print(String name);
}

/**
 * 测试Lambda表达式
 */public class TestShow {
 public static void main(String[] args) {
 //Lambda表达式实现
 //省略了()、参数类型及{}
 Show obj = name-> System.out.println(name);

 //调用obj对象的方法
 obj.print("Tom");
 }
}

25.1.14 总结

  1. 使用要求
  • 方法的参数或局部变量必须为接口类型才能使用Lambda表达式
  • 接口中有且只有一个抽象方法

25.2 函数式接口

25.2.1 概述

使用Lambda表达式开发时,他根本不关心方法的所属类、方法名称及返回值类型。所以为了方便开发,Java提供了一组接口,用户可以直接对接口进行实现。

25.2.2 接口

25.2.3 示例

java
【接口】
`@FunctionalInterfacepublic` interface Function<T, R> {

 /**
 * Applies this function to the given argument.
 *
 * `@param` t the function argument
 * `@return` the function result
 */
 R apply(T t);
}

/**
 * 使用Function接口
 * 需求:计算圆的周长
 */public class TestFunction {
 public static void main(String[] args) {
 //通常情况下我们需要先声明一个自己的接口
 //现在不需要了,因为Jdk中为我们声明了一组接口。所以我们可以直接对接口进行使用
 Function<Integer,Double> fun = r->2*Math.PI*r;

 //调用接口方法
 double girth = fun.apply(8);
 System.out.println("圆的周长为:" + girth);
 }
}

26 设计模式

26.1 概述

所谓设计模式是指前人解决某类问题的实现方式,经过后人整理称为设计模式。

Java中有23中较有名的设计模式。

26.2 构建型模式

26.2.1 单例设计模式

  1. 概述

所谓单例设计模式是指一个类只能实例化出一个对象。

  1. 要素
  • 私有静态字段
  • 构造器私有化
  • 公共静态方法
  1. 分类
  • 懒汉模式
  • 恶汉模式
  1. 懒汉模式
  • 优点

什么时候,什么时候开始创建。

  • 适用

类的应用相对较少的情况下使用懒汉模式。

  1. 饿汉模式
java
/**
 * 饿汉模式
 * 饿了就需要去吃(需要对象时就要去创建)
 */public class Student {
 //声明一个静态对象
 private static Student student =new Student();

 private Student() {
 System.out.println("无参构造函数被调用了.......");
 }

 /**
 * 返回一个当前类型的对象
 */
 public static Student create(){
 return student;
 }

 /**
 * show方法
 */
 public void show(){
 System.out.println("show()方法被调用了.........");
 }
}
  • 优点

缺少判断过程,代码执行效率相对较高

  • 适用

类应用较为频繁时使用。

26.2.2 简单工厂设计模式

  1. 概述

工厂设计模式是构建模式中的一种。在工厂设计模式中工厂负责构建出不同的对象。

  1. 角色分析
  • 产品类:抽象的,定义产品中包含的属性和行为
  • 具体的产品类:产品类的实现
  • 工厂:负责根据条件生成产品
  • 客户:向工厂请求产品
  1. 类图
  2. 代码
java
【抽象产品】
/**
 * 抽象产品
 */public class Calculate {
 /**
 * 计算两个运算数的运算结果
 */
 public int cal(int num1, int num2) {
 return 0;
 }
}
【具体产品1】
public class Add extends Calculate {
 /**
 * 计算两个数相加之和
 */
 `@Override`
 public int cal(int num1, int num2) {
 return num1+num2;
 }
}
【具体产品2】
public class Sub extends Calculate {
 /**
 * 计算两个数之差
 */
 `@Override`
 public int cal(int num1, int num2) {
 return num1-num2;
 }
}
【工厂类】
/**
 * 工厂
 */public class Factory {
 /**
 * 生成
 * `@return`
 */
 public static Calculate create(String symbol){
 //判断运算符符
 switch (symbol){
 case "+":
 return new Add();
 case "-":
 return new Sub();
 case "*":
 return new Times();
 default:
 return null;
 }
 }
}
【客户类】
public class Test {
 public static void main(String[] args) {
 Scanner sc = new Scanner(System.in);

 //提示并接收用户的输入
 System.out.print("运算数1:");
 int n1 = sc.nextInt();
 System.out.print("运算数2:");
 int n2 = sc.nextInt();

 System.out.print("运算符:");
 String symbol = sc.next();

 //调用工厂方法实例化对象
 Calculate cal = Factory.create(symbol);

 if(cal ==null)
 System.out.println("暂不支持该运算");
 else
 System.out.println(n1+symbol+n2+"=" + cal.cal(n1,n2));
 }
}
  1. 优点
  • 将判断抽取到工厂类中,使得代码变得更加清晰简单
  1. 缺点
  • 工厂类中代码过于集中