一. Java 开发环境
JVM(Java Virtual Machine)java 虚拟机主要负责将Java程序经过编译之后生成的和平台无关的字节码文件解释成具体的平台能够识别的机器指令。
JRE(Java Runtime Environment)Java 运行环境JRE = JVM + Java程序执行所需要的核心类库。
JDK(Java Development Kit)Java 开发工具包
JDK = JRE + 开发工具(java、javac…)
JVM 是整个 java 实现跨平台的最核心的部分,但是只有 JVM 并不能运行程序;
如果想要运行一个 Java 程序,可以只安装 JRE,不安装 JDK;
如果想要开发一个 Java 程序 ,必须安装 JDK;
二. Java 语法规范
1. 基本概念
- 类:类是一个模板,它描述一类对象的行为和状态。
- 对象:对象是类的一个实例,有状态和行为。
- 方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。
- 实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。
2. Java 关键字
关键字 :是指在程序中,Java 已经定义好的单词,具有特殊含义。如 public、class、int 等等(关键字中所有字母都为小写)。
3. Java 标识符
标识符:程序中的我们给类、变量以及方法所起的名字。
Java中的类、变量、方法的名称不是随便都可以起的,需要满足一些规则:
① 标识符可以包含名称只能由字母、数字、下划线、$ 符号 。
② 所有的标识符都应该名称只能由字母、下划线、$ 符号开头,不能以数字开头。
③ 标识符不能是关键字。
为了使得程序的可读性更好,一般编写Java程序时,也要满足一定的规范:
① 类名建议采用大驼峰式(首字母大写,后面每个单词首字母大写)。
② 方法名和变量名建议采用小驼峰式(首字母小写,后面每个单词首字母大写)。
③ 常量名建议全部大写,单词之间用下划线分割。
④ 包名、模块名、项目名建议所有字母都小写。
4. Java 编程的注意事项
- Java 是大小写敏感的。
- 源文件名必须和类名相同。(如果文件名和类名不相同则会导致编译错误)。
- 所有的 Java 程序由 public static void main(String[] args) 方法开始执行。
三. Java 基础语法
1. 注释
单行注释: 以 // 开头 换行结束
多行注释: 以 /* 表示注释开始 , 以*/表示注释结束
1 | // 这是一个单行注释 |
2. 数据类型
Java 数据类型分为两大类。
① 基本类型(4 类 8 种)
- 整数型(byte、short、int、long)
- 浮点型(float double)
- 字符型(char)
- 布尔型(boolean)
| 数据类型 | 关键字 | 内存占用/字节 | 取值范围 | 最小值符号 | 最大值符号 |
|---|---|---|---|---|---|
| 字节型 | byte | 1 | -128 ~ +127 | Byte.MIN_VALUE | Byte.MAX_VALUE |
| 短整型 | short | 2 | -32768 ~ +32767 | Short.MIN_VALUE | Short.MAX_VALUE |
| 整型 | int | 4 | -2^31 ~ 2^31-1(超过20亿) | Integer.MIN_VALUE | Integer.MAX_VALUE |
| 长整型 | long | 8 | -2^63 ~ 2^63-1 | Long.MIN_VALUE | Long.MAX_VALUE |
| 单精度浮点型 | float | 4 | 1.4E-45 ~ 3.4028235E38(有效位数6~7位) | Float.MIN_VALUE | Float.MAX_VALUE |
| 双精度浮点型 | double | 8 | 4.9E-324~1.7977E+308(有效位数15位) | Double.MIN_VALUE | Double.MAX_VALUE |
| 字符型 | char | 2 | 0 ~ 65535 | Character.MIN_VALUE | Character.MAX_VALUE |
| 布尔型 | Boolean | 1 | true、false | * | * |
Java 没有任何无符号 (unsigned) 形式的 int、long、short 或 byte 类型,但可以通过其包装类转换得到。
基本数据类型必须进行初始化,没有默认值。
基本类型之间可以进行转换,分为自动类型转换和强制类型转换。
自动类型转换(将容量小的数据类型转换为容量大的数据类型)
注意:
- 不能对 boolean 类型进行类型转换。
- byte,short,char 之间不会相互转换,他们三者在计算时首先转换为int类型。
- 当把任何基本数据类型的值和字符串 (String) 进行连接运算时 (+),基本数据类型的值将自动转化为字符串(String) 类型。
强制类型转换(将容量大的数据类型转换为容量小的数据类型)格式:级别低的数据类型 变量名 = (级别低的数据类型) 级别高的数据类型;
1
如:int num = (int)10.5;// 结果为 10,直接砍掉小数部分。
注意:强制类型转换可能造成精度降低或溢出
② 引用类型
- 类(class)
- 接口(interface)
- 数组(array)
3. 常量
常量是在程序运行时不能被修改的量。通常使用大写字母表示常量。
在 Java 中,利用关键字 final 指示常量,比如 final int a = 10;
整数常量
十进制: 正常的写法,如 5,-125 等。
二进制: 在数字前面加前缀 0B 或 0b,如 0B11,0B110等。
八进制: 在数字前面加前缀 0,如 05,01等。
十六进制: 在数字前面加前缀 0x 或 0X,如 0XA,0x3等。
浮点数常量:如 0.1,10.8等等。
字符常量: 如 ‘A’,’C’ 等。
字符串常量: 如 “zhang”,”A” 等。
布尔常量: 只有 true 和 false。
空常量: null。
4. 变量
变量是在程序运行时能被修改的量。通常变量名采用小驼峰式(首字母小写,后面每个单词首字母大写)。
变量的声明:
1 | //规则:数据类型 变量名; |
变量的初始化一般有两种方法:
1 | /* 第一种变量赋值方法,在声明的时候直接赋值。 |
注意:
- 整数默认为 int 类型,如果需要的数据类型是 long,那么需要在数据后加后缀 L 或 l,如 long a = 10L;
- 浮点数默认为 double 类型,如果需要的数据类型是 float,那么需要在数据后加后缀 F 或 f,如 float a = 0.5F;
- 变量未赋值时不可使用;
- 不可重复定义变量。
- 从Java 10 开始,对于局部变量,如果可以从变量的初始值推断出它的类型,就不再需要声明类型。只需要使用关键字 var 而无需指定类型,比如:var vacationDays =12; 整型
5. 运算符
① 运算符的分类
算数运算符(+、-、*、/、%、++、–)
1
/*注意:整数除法的结果仍然会是整数,规则是直接砍掉小数取整,而不是四舍五入。*/
关系运算符(>、<、<=、>=、==、!=)
1
/*注意:关系运算符用于比较两个数据的大小,结果会产生一个布尔(boolean)值。如果关系为真,则结果为 true,如果关系为假,则结果为false。*/
逻辑运算符(&&、||、!)
1
2
3
4
5
6/* 注意:逻辑运算符的运算结果只能是一个布尔值(true/false); && 和 || 具有短路现象,
当&&时,如果第一个表达式已经为false,不管第二个表达式为true/false,结果一定为false ,此时第二个表达式将不会再执行。
当||时,如果第一个表达式已经为true,不管第二个表达式为true/false,结果一定为true,此时第二个表达式将不会再执行。*/
int a = 5;
boolean flag = (3>5) && (++a == 6 )
System.out.println(a);//由于短路现象,++a表达式并没有被执行,因此输出结果为5条件运算符(也称三目运算符)
1
2
3
4
5
6
7
8
9
10/* 条件运算符语法规则: 数据类型 变量名 = 布尔表达式?结果1:结果2;
条件运算符含义:如果布尔表达式为true,则执行变量名=结果1,否则执行变量名=结果2。 */
int a=2,b=4;
int num = a < b ? a : b;//因为a<b为真,故将执行结果1,即num=a,则可得结果为num=2
/*条件运算符相比于if else语句更加简洁,且执行效率更高,但运算完必须要有一个结果。而if else运算完不需要一个结果。比如三元运算符内不能单独列出输出表达式。凡是可以使用三元运算符的地方,都可以改写为if-else;反之,不成立。*/
if (num > 0) {
System.out.println("num是正数");
}else{
System.out.println("num是负数");
}//不能使用条件运算符代替赋值运算符(=、+=、-=、*=、/=、%=)
1
/* a + = b; 等价于 a = a + b ; */
位运算符(&、|、~、^、<<、>>)
② 运算符的优先级(算、关、逻、条、赋)
6. 流程控制
(1)顺序结构
1 | // 顺序执行,即根据编写的顺序,从上到下运行,没有跳转。 |
(2)选择结构
if 选择结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 第一种:if,如果布尔表达式为真,则执行语句体,否则不执行。
if(布尔表达式){
语句体;
}
// 第二种:if-else,如果布尔表达式为真,则执行语句体1,否则执行语句体2.
if(布尔表达式){
语句体1;
}else{
语句体2;
}
// 第三种:if—else if—...-else
if(布尔表达式){
语句体1;
}else if {
语句体2;
}
...
else{
语句体n+1;
}switch 选择结构
1
2
3
4
5
6
7
8
9
10
11
12switch(表达式){
case 常量1:
语句体1;
break;
...
case 常量n:
语句体n;
break;
default:
语句n+1;
break;
}case 标签可以为:
- 类型为 char、byte、short、或 int 的常量表达式
- 枚举常量
- 从Java 7开始,还可以是字符串字面量。
(3)循环结构
for循环1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/*
for(初始化表达式①;布尔表达式②;递进表达式③){
循环体④;
}
注意:
执行顺序:①②④③-②④③-②④③-...
当布尔表达式为假时,循环结束,执行后续语句。
*/
// 示例:计算1+2+..+100的值
public static void main(String[] args) {
int sum = 0;
for (int i = 1; i < 101; i++) {
sum+=i;
}
System.out.println("1+2+..+100的结果为:"+sum);// 1+2+..+100的结果为为:5050
}while循环1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/*
while(布尔表达式){
循环体;
}
注意:当布尔表达式为假时,循环结束,执行后续语句。
*/
// 示例:计算1+2+..+100的值
public static void main(String[] args) {
int sum = 0;
int i = 1;
while (i <= 100) {
sum += i;
i++;
}
System.out.println("1+2+..+100的结果为:" + sum);// 1+2+..+100的结果为为:5050
}do while循环1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/*
do{
循环体;
}while(布尔表达式);
注意:
do while中的循环体至少执行一次;
当布尔表达式为假时,循环结束,执行后续语句。
*/
// 示例:计算1+2+..+100的值
public static void main(String[] args) {
int sum = 0;
int i = 1;
do {
sum+=i;
i++;
}while(i<=100);
System.out.println("1+2+..+100的结果为:" + sum);//1+2+..+100的结果为为:5050
}break和continue- break 和 continue 均为循环控制语句;
- break 用于终止当前整个循环;
- continue 用于跳出本次循环,转而去判断是否需要执行下次循环。
1
2
3
4
5
6
7
8
9
10
11// 示例:计算2+4+..+100的值
public static void main(String[] args) {
int sum = 0;
for (int i = 0; i < 101 ; i++) {
if( i % 2!=0 ){
continue;
}
sum+=i;
}
System.out.println("2+4+..+100的结果为:" + sum);//2+4+..+100的结果为:2550
}
7. 数组
数组是一组有序的具有相同数据类型的元素序列。
数组的特点:
- 数组是一种引用数据类型;
- 数组当中的多个数据,类型必须统一;
- 数组的长度一旦指定,不可更改。
数组的初始化:
1 | /* |
数组的使用:
数组元素的引用方式:数组名[数组元素下标];
- 数组元素下标(也称为索引)从 0 开始;长度为 n 的数组合法下标取值范围: 0~n-1;
- 数组的长度:通过使用【数组名 .length】 可以获取数组的长度;
- 数组名是该数组所在内存的地址值;
- new 出来的东西在内存的堆区;
- 数组如果没有初始化,会有默认值(数字默认全为0,boolen默认为false)。
1 | // 示例:定义一个数组,存放1,2,3,4,5,并将数组元素遍历打印。 |
四. Java 面向对象
1. 类和对象
(1)基本概念
- 类:类是一个模板,它描述一类对象的行为和状态。
- 对象:对象是类的一个实例,有状态和行为。
- 属性 :一个类的状态信息,对应类中的成员变量。
- 行为 :一个类能够做什么,对应类中的成员方法
(2)类的定义
1 | /* |
(3)对象的创建与使用
1 | /* |
(4)成员变量
1 | /* |
★ 成员变量与局部变量的区别:
| 在类中的位置 | 作用范围 | 初始化值 | 修饰符 | 内存位置 | |
|---|---|---|---|---|---|
| 成员变量 | 类中,方法外 | 类中 | 有默认值 | public、private、static、final 等 | 堆区 |
| 局部变量 | 方法中或者方法声明上 (形式参数) | 方法中 | 没有默认值。必须先定义,赋值,最后使用 | 不能用权限修饰符修饰,可以用 final 修饰 | 栈区 |
(5)成员方法
1 | /* |
方法的的重载:
- 概念:在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。
- 特点:与返回值类型无关,只看参数列表,且参数列表必须不同(个数不同 / 数据类型不同 / 顺序不同)。
- 调用:通过方法的参数列表,调用不同的方法。
1 | // 示例:创建一个sum方法,可以计算两个整数或三个整数的和 |
形参个数可变的方法:
- 声明格式:方法名(参数的类型名 …参数名)
- 可变参数方法的使用与方法参数部分使用数组是一致的;
- 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个;
- 可变个数形参的方法与同名的方法之间,彼此构成重载;
- 方法的参数部分有可变形参,需要放在形参声明的最后;
- 在一个方法的形参位置,最多只能声明一个可变个数形参。
1 | // 示例:定义形参可变的方法,并与定义数组形式的新参方法做对比。 |
方法的参数传递机制:
- Java 里方法的参数传递方式只有一种:值传递。(即将实际参数值的副本传入方法内,而参数本身不受影响。)
- 如果形参是基本数据类型,则将实参基本数据类型变量的“
数据值”传递给形参; - 如果形参是引用数据类型,则将实参引用数据类型变量的“
地址值”传递给形参;
1 | public class Test { |
(6)static 关键字
static 关键字可以用来修饰:属性、方法、内部类、代码块;被修饰的成员是从属于类的,而不是单单是属于某个对象的。既然属于类,就可以不靠创建对象来调用了,而直接通过类进行调用。
静态变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27/*
概念:
当static修饰成员变量时,该变量称为静态变量,也称为类变量。
格式:
static 数据类型 变量名;
注意:
① 如果一个成员变量使用了static关键字,那么这个变量不再属于对象自己,而是属于所在的类。多个对象共享同一份数据。
② 任何对象都可以更改静态变量的值,但也可以在不创建该类的对象的情况下,通过【类.属性】的方式对静态变量进行操作。
③ 当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
④ 静态变量随着类的加载而加载,它的加载要早于对象的创建。由于类只会加载一次,则静态变量在内存中也只会存在一份,而且是存在方法区的静态域中。
*/
public class Test {
static int num = 10;
}
public class Main {
public static void main(String[] args) {
Test test1 = new Test();
Test test2 = new Test();
Test.num = 20;
test1.num = 30;
test2.num = 40;
System.out.println(Test.num); //40
System.out.println(test1.num); //40
System.out.println(test2.num); //40
}
}★ 静态变量与普通成员变量的区别
所属 存储区域 生命周期 调用方式 静态变量 类 方法区 与类的生命周期相同 类.属性 / 对象.属性 普通成员变量 某个对象 堆区 与其所属对象的生命周期相同 所属对象.属性 静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/*
概念:
当static修饰成员方法时,该变量称为静态方法,也称为类方法。
格式:
修饰符 static 返回值类型 方法名 (参数列表){
方法体;
}
注意:
① 在静态的方法内,不能使用this关键字、super关键字。
② 静态方法随着类的加载而加载,可以通过【类名.静态方法】进行调用,也可以通过【对象名.静态方法】调用。
③ 静态方法中,只能调用静态的方法或变量。
④ 非静态方法中,既可以调用非静态的方法或变量,也可以调用静态的方法或变量。
*/
public class Test {
public static void staticMethod() {
System.out.println("这是一个静态方法");
}
}
public class Main {
public static void main(String[] args) {
Test test = new Test();
Test.staticMethod(); // 这是一个静态方法
test.staticMethod(); // 这是一个静态方法
}
}
2. 封装
(1)高内聚低耦合
好的程序设计应该具有的特性:
高内聚:类的内部数据操作细节自己完成,不允许外部干涉;低耦合:仅对外暴露少量的方法用于使用。
(2)封装的好处
- 良好的封装能够减少耦合。
- 隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性以及安全性。
- 可以对成员变量进行更精确的控制。(使用者对类内部定义的属性直接操作可能会导致数据的错误、混乱或安全性问题。)
(3)访问权限修饰符
Java 权限修饰符 public、protected、default(缺省)、private 置于类的成员定义前,用来限定对象对该类成员的访问权限。
- public : 对所有类可见。使用对象:类、接口、变量、方法
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
以下是访问控制级别:
public > protected > (default) > private
| 修饰符 | 同一个类 | 同一个包 | 不同包的子类 | 同一个工程 |
|---|---|---|---|---|
| public | √ | √ | √ | √ |
| protected | √ | √ | √ | |
| default(缺省) | √ | √ | ||
| private | √ |
注意:对于 class 的权限修饰只可以用 public 和 default(缺省)。
(4)实现封装的步骤
- 使用 private 关键字来修饰成员变量(成员变量私有化)。
- 对需要访问的成员变量,提供对应的一对公共的(public)setter 方法 、getter 方法实现对该成员变量的操作(提供对外接口)。
- setter、getter 方法的名称应该为 private 修饰的成员变量的首字母大写,并在前面添加 get、set,比如一个private的成员变量名为 age,那么其 setter、getter 方法的名称为 setAge、getAge。
- 对于 setter 来说,不能有返回值,参数类型和成员变量对应(用于给变量赋值)
- 对于 getter 来说,不能有参数,返回值类型和成员变量对应(用于获取变量值)
1 | // 以学生类为示例,说明封装的步骤 |
(5)构造方法
1 | /* |
(6)this关键字
- 当方法的局部变量和类的成员变量重名的时候,根据“就近原则”,会优先使用局部变量。如果需要访问本类当中的成员变量,需要使用格式:
this.成员变量名 - this 关键字指向的是当前对象的引用(this 理解为:当前对象或当前正在创建的对象)
1 | // 使用this关键字区分局部变量和成员变量 |
(7)标准代码——JavaBean
JavaBean 是一种可重用的Java组件,它可以被 Applet、Servlet、JSP 等 Java 应用程序调用,也可以可视化地被Java开发工具使用。
一个标准的类(也叫做JavaBean)通常要拥有下面四个组成部分:
- 所有的成员变量都要使用 private 关键字修饰;
- 为每一个成员变量编写一对儿 getter/setter 方法;
- 编写一个无参数的构造方法;
- 编写一个全参数的构造方法。
1 | // 定义一个标准的学生类 |
(8)代码块
在 Java 中,使用{}大括号括起来的代码被称为代码块。
普通代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 最常见的一种代码块,有方法名称,即类中方法的方法体,可以控制变量的生命周期,提高内存利用率。
public class Test {
public static void Method() { // 普通代码块
System.out.println("这是一个普通代码块");
}
}
public class Main {
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.Method);// 这是一个普通代码块
}
}构造代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/*
定义:
在类中直接定义没有任何修饰符、前缀、后缀的代码块即为构造代码块,其可以给所有对象进行初始化。
注意:
>构造代码块内部可以输出语句;
>每创建一个对象,就执行一次构造代码块,再执行构造方法;
>如果一个类中定义了多个构造代码块,则按照声明的先后顺序执行,(一般也不会构造多个);
>构造代码块内可以调用静态变量、静态方法、普通成员变量、普通成员方法。
*/
public class Test {
int num;
{ //构造代码块
num = 10;
System.out.println("这是一个构造代码块");
}
}
public class Main {
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.num);//这是一个构造代码块\n10
}
}静态代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/*
定义:
在类中直接定义的只有static关键字修饰的代码块即为静态代码块,其可以对静态属性、类进行初始化,并且只执行一次。
注意:
>静态代码块内部可以输出语句;
>静态代码块随着类的加载而执行,而且只执行一次,优先于main方法和构造方法的执行;
>如果一个类中定义了多个静态代码块,则按照声明的先后顺序执行;
>静态代码块的执行要优先于非静态代码块的执行;
>静态代码块内只能调用静态变量和静态方法。
*/
public class Test {
static int num;
static {
num = 10;
System.out.println("这是一个静态代码块");
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Test.num);//这是一个静态代码块\n10
}
}
3. 继承
(1)继承的概念
子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。
(2)继承的优缺点
优点:代码共享,减少重复代码。
- 减少了代码冗余,提高了代码的复用性。
- 提高了代码的维护性,更有利于功能的扩展。
- 让类与类之间产生了关系,是多态的前提
缺点:提高了类的耦合性,使得代码之间的联系更加紧密,那么代码独立性更差。
(3)继承的格式
1 | // 使用extends关键字。 |
(4)继承的特点
子类拥有父类非 private 的属性、方法。
子类可以拥有自己特有的属性和方法,即子类可以对父类进行扩展。
子类可以对父类的方法进行重写,即子类可以用自己的方式实现父类的方法。
顶层父类是Object类。所有的类默认继承Object作为父类。
java支持单继承、多级继承、不同类继承同一个类,不支持多继承

(5)重写
重写(也称为覆盖):子类对父类方法的实现过程进行重新编写。
1 | public class 父类名称{ |
重写的好处:子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
重写的要求:
子类重写的方法必须和父类被重写的方法具有相同的方法名称和参数列表。
父类被重写的方法的返回值类型是 void,则子类重写的方法的返回值类型只能是 void
父类被重写的方法的返回值类型是 A 类型,则子类重写的方法的返回值类型可以是 A 类或 A 类的子类
父类被重写的方法的返回值类型是基本数据类型,则子类重写的方法的返回值类型必须是相同的基本数据类型(一模一样)。
子类方法的权限必须【大于等于】父类方法的权限修饰符。public > protected > (default) > private
子类不能重写父类中声明为 private 权限的方法。
子类方法抛出的异常不能大于父类被重写方法的异常。
在子类的重写方法前,可以加一个注解 @override,用来检测是不是有效的正确覆盖重写。
重写的特点:创建的是子类对象,则优先用子类方法。
重写与重载的区别★★★:
重载:同一个类中不同方法具有相同的名字,但是参数不一样,即参数的名称和参数的类型不一样。同类不同参。
重写:子父类的,即子类与父类具有相同的方法名字还有参数参数相同和相同的返回类型。即同名同参同类型。
| 参数列表 | 是否有继承关系 | |
|---|---|---|
| 重载(overload) | 必须不同 | 无继承关系,在同一个类中 |
| 重写(override) | 必须相同 | 有继承关系,在不同类中 |
(6)super 关键字
如果子类想要调用父类中的内容,可以使用super关键字,主要有下面三种用法:
① super 可用于访问父类中定义的成员变量【因为成员变量时私有的,所有子类不能直接调用父类的,需要用到super关键字】
② super 可用于调用父类中定义的成员方法
③ super 可用于在子类构造器中调用父类的构造器
注意:
- 尤其当子父类出现同名成员时,可以用 super 表明调用的是父类中的成员
- super 的追溯不仅限于直接父类
- super 和 this 的用法相像,this 代表本类对象的引用,super 代表父类的内存空间的标识
super 关键字的使用:
① 访问成员变量★
this.成员变量 ->本类的
super.成员变量 ->父类的
1 | public class A{ |
② 访问成员方法★
this.成员方法名() ->本类的
super.成员方法名() ->父类的
1 | public class A { |
③ 访问构造方法★
this.(形参列表) ->本类的
super(形参列表) ->父类的
1 | /* |
总结:★ this 和 super 的区别
| 关键字 | 访问成员变量 | 调用成员方法 | 调用构造方法 |
|---|---|---|---|
| this | 访问本类中的成员变量,如果本类没有,则从父类中继续查找 | 访问本类中的成员方法,如果本类没有,则从父类中继续查找 | 访问本类中的构造方法 |
| super | 直接访问父类中的成员变量 | 直接访问父类中的成员方法 | 访问父类中的构造方法 |
在主函数中,创建具有继承关系的对象时,
如果成员变量或者静态方法重名,看等号左边是谁,则优先用谁,没有则向上找。(编译看左边,运行看左边)
如果成员方法重名,看 new 的谁,则优先用谁,没有则向上找。(编译看左边,运行看右边)
4. 多态
(1)多态的概念
同一行为发生在不同的对象上会产生不同的结果。(同一行为,具有多个不同表现形式)
(2)多态的优缺点
优点:提高了代码的扩展性,前期定义的代码可以使用后期的内容,而且可以接口复用(子类共用一个父类接口)。
缺点:多态不能访问子类特有的功能( 前期定义的内容不能使用(调用)后期子类的特有内容。)
(3)多态的使用
使用多态的前提:
① 存在继承或者实现关系
② 子类覆盖(重写)父类方法
③ 向上转型(父类引用指向子类对象)
1 | /* |
(4)instanceof 关键字
instanceof 关键字用于来判断多态中父类引用的对象,原本是哪个子类(判断某个对象是否是某个 Class 类的实例)。
1 | /* |
(5)引用类型转换
向上转型
当父类引用指向一个子类对象时,便是向上转型。
1
2
3
4
5/*
多态本身是子类类型向父类类型向上转换的过程,这个过程是默认的。(类似于基本类型中的自动类型转换)
使用格式:
父类类型 变量名 = new 子类类型();
*/向下转型
一个已经向上转型的子类对象,将父类引用再转为子类引用,可以使用强制类型转换的格式,便是向下转型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66/*
父类类型向子类类型向下转换的过程,这个过程是强制的。(类似于基本类型中的强制类型转换)
使用格式:
变量名 = (子类类型) 父类变量名;
向下转型的目的:
多态的一个缺点就是不能调用子类中特有的方法。而如果想要调用子类特有的方法,就需要向下转型。
向下转型的注意事项:
使用强转时,可能出现ClassCastException的异常,出现异常的原因是多态中父类引用的对象,原本不是此子类,却想要转型为此子类。为了避免在向下转型时出现ClassCastException的异常,在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
*/
// 创建一个父类Animal
public class Animal {
public void eat() {
System.out.println("动物在吃饭");
}
}
// 创建一个子类Cat
public class Cat extends Animal {
public void eat() {
System.out.println("猫吃鱼");
}
// 猫特有的方法
public void catchMouse() {
System.out.println("猫抓老鼠");
}
}
// 创建一个子类Dog
public class Dog extends Animal {
public void eat() {
System.out.println("狗吃骨头");
}
// 狗特有的方法
public void keepHouse() {
System.out.println("狗看家");
}
}
// 创建主方法
public class Main {
public static void main(String[] args) {
Animal animal = new Cat(); // 向上转型
// Dog dog = (Dog)animal; 语句错误,本来new的是Cat,却要转换为Dog,会出现ClassCastException
// Cat cat = (Cat)animal; //向下转型
function(animal);
}
public static void function(Animal animal) { // 接口复用
if (animal instanceof Cat) { // 如果animal对象原本属于Cat类,则进行向下转型,并调用Cat特有的方法
Cat cat = (Cat) animal;
cat.catchMouse();
}
if (animal instanceof Dog) { // 如果animal对象原本属于Dog类,则进行向下转型,并调用Dog特有的方法
Dog dog = (Dog) animal;
dog.keepHouse();
}
}
}
// 由于定义的animal原本属于Cat类,故最终结果为:猫抓老鼠
(6)final关键字
final 关键字代表最终、不可改变的,可以用来修饰:类、方法、变量。
final 修饰类
1
2
3
4
5
6
7/*
格式:
final class 类名 {
}
注意:
被final修饰的类,不能被继承。比如:String类、System类、StringBuffer类
*/final 修饰方法
1
2
3
4
5
6
7
8/*
格式:
修饰符 final 返回值类型 方法名(参数列表){
//方法体
}
注意:
被final修饰的方法,不能被重写。比如:Object类中getClass();
*/final 修饰变量
① 修饰局部变量——基本类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/*
格式:
方式一:
final 数据类型 变量名;
变量名 = 值;
方式二:
final 数据类型 变量名 = 值;
注意:
被final修饰的局部变量,只能被赋值一次。
被final修饰的常量名称,一般都有书写规范,所有字母都大写。
*/
public class Test {
public static void main(String[] args) {
final int NUM; //声明变量,使用final修饰
NUM = 10; //赋值
//NUM = 20; // 报错,不可重新赋值
final int NUM1 = 10;// 声明变量,直接赋值,使用final修饰
//NUM1 = 20; // 报错,不可重新赋值
}
}② 修饰局部变量——引用类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/*
格式:
final 引用数据类型 变量名 = new 引用数据类型(参数列表);
注意:
引用类型的局部变量,被final修饰后,只能指向一个对象,地址不能再更改。
*/
public class Test {
public void method(){
System.out.println("引用类型final关键字的测试");
}
}
public class Main {
public static void main(String[] args) {
final Test test = new Test();
//test =new Test();//报错,不能指向新的对象。
test.method();//引用类型final关键字的测试
}
}③ 修饰成员变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14/*
成员初始化方式有两种,只能二选一:
①显示初始化;
public class Test {
final int NUM = 10;
}
②构造方法初始化。
public class Test {
final int NUM;
public Test(int num) {
this.NUM = num;
}
}
*/
5. 抽象
(1)抽象类
用 abstract 关键字来修饰一个类,这个类就叫做抽象类。
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类;
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
1 | /* |
(2)抽象方法
用 abstract 关键字来修饰一个方法,这个方法就叫做抽象方法。
父类中的方法,被它的子类们重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,而方法主体则没有存在的意义了。我们把没有方法主体的方法称为抽象方法。
1 | /* |
6. 接口
Java 接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。
类的内部封装了成员变量、构造方法和成员方法,而接口的内部封装了常量和抽象方法(JDK 7及以前)、默认方法和静态方法(JDK 8)、私有方法(JDK 9)。
1 | /* |
★ 抽象类与接口的对比
- 相同点
- 均不能被实例化。
- 都可以包含抽象方法。
- 接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
- 不同点
- 抽象类中有构造方法,而接口中没有。
- 抽象类不能多继承,而接口可以。
- 接口中的变量必须有 static、final 修饰,实际是一个常量,必须赋初值,而抽象类可以任意。
7. 内部类
如果将一个类定义在另一个类里面或者一个方法里面,这样的类就称为内部类。内部类又可以分为成员内部类、局部内部类、匿名内部类和静态内部类。
(1)成员内部类
成员内部类是最普通的内部类,它的定义为位于另一个类的内部。在描述事物时,若一个事物内部还包含其他事物,就可以使用内部类这种结构。比如,汽车类 Car 中包含发动机类 Engine ,这时,Engine 就可以使用成员内部类来描述。
内部类仍然是一个独立的类,在编译之后会内部类会被编译成独立的 .class文件,但是前面冠以外部类的类名和$符号 。
1 | /* |
(2)局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
1 | /* |
(3)匿名内部类
匿名内部类是内部类的简化写法。它的本质是一个带具体实现的【父类或者父接口】的匿名的子类对象。开发中,最常用到的内部类就是匿名内部类。
1 | /* |
(4)静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。
1 | /* |
五. Java常用API
API(Application Programming Interface),应用程序编程接口。它是 JDK 中提供给使用者的一些常用类的说明文档,使用者不需要访问源码或理解内部工作机制的细节,只需要会使用即可。在查看 API 文档时,使用者需要查看当前类的包路径、构造方法以及方法摘要。
引用类型的一般使用步骤:
①
导包。import 包路径.类名称;- 如果需要使用的目标类,和当前类位于同一个包下,则可以省略导包语句不写。
- 如果使用的类或接口是 java.lang 包下定义的,则可以省略 import 结构。
- 可以使用 “xxx.*” 的方式,表示可以导入 xxx 包下的所结构。
- 如果在源文件中,使用了不同包下的同名的类,则必须至少一个类需要以全类名的方式显示。
②
创建。类名称 对象名 = new 类名称 ( 参数列表 );③
使用。对象名.成员方法名 ();
1. Object 类
- Object 类在 java.lang 包下,导包语句可以省略不写
- java.lang.Object 类是 Java 语言中的根类,即所有类的父类。
- 如果一个类没有特别指定父类,那么默认则继承自 Object 类。
- Object 类只声明了一个空参的构造器
- Object 类中的所有方法,子类均都可以使用。(equals() 、toString() 、getClass() 、hashCode()…)
1 | /* |
1 | /* |
2. Scanner类
1 | /* |
3. Random类
1 | /* |
4. String类
1 | /* |
5. StringBuffer、StringBuilder
String 类对象一旦创建不可改变,如果想要对字符串进行修改,并且不产生新的对象,可以使用 StringBuffer 和 StringBuilder 类。
StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。
由于 StringBuilder 相较于 StringBuffer 有速度优势,所以多数情况下建议使用 StringBuilder 类。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
1 | /* |
6. Arrays 类
1 | /* |
7. Math 类(包含 BigDecimal)
1 | /* |
8. 包装类
为了使基本数据类型的变量具有类的特征,引入了包装类的概念。基本数据类型及其对应的包装类如下所示:
| 基本类型 | 对应的包装类(位于java.lang包中) |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
- 基本数据类型与对应的包装类对象之间的转换【记住自动装箱与自动拆箱】
1 | /* |
- 基本数据类型与字符串之间的转换
1 | /* |
包装类是不可变的,即一旦构造了包装类,就不允许更改包装在其中的值,同时包装类还是 final,因此不能派生它们的子类。
9. 日期时间类
Date类
1 | /* |
DateFormat类
1 | /* |
Calendar类
1 | /* |
10. java 比较器
在Java 中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题,此时不能直接通过关系运算符(>、<、<=、>=、==、!=)进行比较,java 有三种实现对象比较的方法:
1)重写 Object 类的 equals() 方法,但不能比较大小,只能比较是否相等。
2)自然排序:继承 Comparable 接口,并实现 compareTo() 方法;
3)定制排序:定义一个单独的对象比较器,继承自 Comparator 接口,实现 compare() 方法。
1 | /*自然排序 |
1 | /*定制排序 |
六. 异常
1. 异常的概念
异常是指程序在执行过程中,出现的非正常的情况,最终会导致 JVM 的非正常停止。在 Java 等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java 处理异常的方式是中断处理。
2. 异常的分类
Throwable:Throwable 类是 Java 语言中所有错误或异常的超类。类中常用方法有:
public void printStackTrace():打印异常的详细信息。
包含了异常的类型,异常的原因,还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace。
public String getMessage() :获取发生异常的原因。
提示给用户的时候,就提示错误原因。
Error:程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。比如栈溢出、堆溢出等错误。
Exception:程序本身可以捕获并且可以处理的异常。可以分为两大类:
- 非受检异常(运行时异常):指 RuntimeException 类及其子类异常,编译器不会检查此类异常,并且不要求处理异常。程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。比如数组索引越界异常,空指针异常等等。
- 受检异常(编译时异常):是 RuntimeException 以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。
3. 常见异常
java.lang.ArrayIndexOutOfBoundsException数组索引越界异常。1
2
3
4
5
6
7//用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
public class Main {
public static void main(String[] args) {
int[] array = new int[10];
System.out.println(array[10]);//报错
}
}java.lang.NullPointerException空指针异常1
2
3
4
5
6
7/*当应用程序试图在需要对象的地方使用null时,抛出该异常。这种情况包括:调用 null 对象的实例方法、访问或修改null对象的字段、将null作为一个数组,获得其长度、将null作为一个数组,访问或修改其时间片、将null作为Throwable值抛出。*/
public class Main {
public static void main(String[] args) {
String string = null;
System.out.println(string.equals(null));//报错
}
}java.lang.ClassCastException类型转换异常1
2
3
4
5
6
7
8//当试图将对象强制转换为不是实例的子类时,抛出该异常。
public class Main {
public static void main(String[] args) {
Object obj = new String("a"); //多态
Math math = (Math) obj; //编译不报错,但运行会报错
System.out.println(math);
}
}java.lang.ArithmeticException算术异常1
2
3
4
5
6//当出现异常的运算条件时,抛出此异常。例如,一个整数“除以零”时,抛出此类的一个实例。
public class Main {
public static void main(String[] args) {
System.out.println(1 / 0);//除0
}
}java.lang.NumberFormatException数字格式异常1
2
3
4
5
6
7
8
9
10
11//当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
public class Main {
public static void main(String[] args) {
String string1 = "123";
String string2 = "abc";
Integer integer1 = Integer.valueOf(string1);
Integer integer2 = Integer.valueOf(string2);//编译不报错
System.out.println(integer1);
System.out.println(integer2);//运行报错
}
}
4. 异常的处理
异常的处理分为两步:
第一步:抛出异常。程序在正常执行的过程中,如果出现异常,会在异常代码处生成一个对应异常类的对象,并将此对象抛出,其后的代码就不再执行。对于异常对象的产生分为两种:① 系统自动生成的异常对象 ② 手动的生成一个异常对象,并抛出(throw)
第二步:捕获异常。捕获异常分为两种:① try…catch…finally 代码块直接处理 ② 通过 throws 进行声明,让调用者去处理。
(1)try…catch…finally
通过 try…catch…finally 代码块可以对出现的异常进行指定方式的处理。
1 | /* |
(2)throws
如果一个方法中的语句执行时可能生成某种异常,但是并不能确定如何处理这种异常或者由调用者处理更好,则此方法应显示地通过 throws 关键字声明抛出异常,表明该方法本身将不对这些异常进行处理,而由该方法的调用者负责处理。
1 | /* |
(3)throw
Java 异常类对象除在程序执行过程中出现异常时由系统自动生成并抛出,也可根据需要使用人工创建并抛出 ,即通过 throw 语句手动显式的抛出一个异常。throw 语句的后面必须是一个异常对象
1 | /* |
throw 和 throws 的区别:
throw 表示抛出一个异常类的对象,生成异常对象的过程。声明在方法体内。
throws 属于异常处理的一种方式,声明在方法的声明处。
5. 自定义异常类
在开发中根据自己业务的异常情况来自定义异常类。
1 | /* |
七. 多线程
1. 程序、进程与线程
程序:一组计算机能识别和执行的指令,运行于电子计算机上,满足人们某种需求的信息化工具。即一段静态的代码。
进程:一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。进程是系统进行资源分配和调度的基本单位,
线程:进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。线程是程序执行的最小单位。
2. 多线程的创建
方式一:继承 Thread 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43/*
步骤:
① 创建一个继承于Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
② 创建Thread子类的对象。
③ 调用子类对象的start()方法来启动该线程。
注意:
① 启动一个线程,必须调用start(),不能调用run()的方式启动线程。
② 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出异常“IllegalThreadStateException”。
③ 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start().
★Thread类的构造方法:
Thread() :分配一个新的线程对象。
Thread(String name) :分配一个指定名字的新的线程对象。
Thread(Runnable target) :分配一个带有指定目标新的线程对象。
Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
★Thread类的常用方法:
String getName() :获取当前线程名称。
void setName(String name) :改变线程名称,使之与参数 name 相同
void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
void run() :此线程要执行的任务在此处定义代码。
void join() :等待该线程终止。(在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态)
static void yield() :暂停当前正在执行的线程对象,并执行其他线程(释放当前CPU的执行权)。
static void sleep(long millis) :在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。
static Thread currentThread() :返回对当前正在执行的线程对象的引用。
*/
public class MyThread1 extends Thread {// 第一步:创建一个继承于Thread类的子类,并重写该类的run()方法
public void run() {
for (int i = 0; i < 10 ; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread1 m1 = new MyThread1(); // 第二步:创建Thread子类对象
m1.start(); // 第三步:启动分线程
for (int i = 0; i < 10 ; i++) {
System.out.println(Thread.currentThread().getName() +":" + i);
}
}
}方式二:实现 Runnable 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28/*
步骤:
① 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
② 创建Runnable实现类的对象。
③ 将实现类对象作为Thread类构造方法的参数,创建Thread对象,该Thread对象才是真正的线程对象。
④ 调用Thread对象的start()方法来启动线程。
注意:在开发中,相较于方式一,优先选择实现Runable接口的方式,因为可以实现“多继承”。
*/
public class MyThread2 implements Runnable { // 第一步:定义Runnable接口的实现类,并重写该接口的run()方法
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread2 m2 = new MyThread2();// 第二步:创建Runnable实现类的对象
Thread thread = new Thread(m2); // 第三步:创建Thread类的对象,构造方法参数为Runnable实现类对象
thread.start(); // 第四步:通过start()方法启动线程。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}方式三:实现 Callable 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/*
步骤:
① 创建Callable接口的实现类 ,并实现Call方法。
② 创建Callable实现类的对象。
③ 将Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
④ 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象。
⑤ 调用Thread对象的start()方法来启动线程。
注意:
① call()方法是有返回值的,可以通过FutureTask的get()方法获取返回值。
② call()可以抛出异常,被外面的操作捕获,获取异常的信息
③ Callable是支持泛型的。
*/
public class MyThread3 implements Callable{ // 第一步:创建Callable接口的实现类 ,并实现Call方法。
public Object call() throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
return null;// 注意有返回值。
}
}
public class Main {
public static void main(String[] args) {
MyThread3 m3 = new MyThread3(); // 第二步:创建Callable实现类的对象。
FutureTask f = new FutureTask(m3);// 第三步:创建FutureTask的对象,Callable实现类的对象为参数
Thread thread = new Thread(f);// 第四步:创建Thread类的对象,FutureTask类的对象为参数
thread.start();// 第五步:通过start()方法启动线程。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
try {
System.out.println(futureTask.get()); // 获取call()方法的返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}方式四:线程池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31/*
概念:一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
好处:
① 降低资源消耗。(减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务)
② 提高响应速度。(当任务到达时,任务可以不需要等到线程创建就能立即执行)
③ 提高线程的可管理性。(可以调整线程池中工作线线程的数目,防止内存消耗过多而出现故障)
步骤:
1. 创建线程池对象。(提供指定线程数量的线程池)
2. 创建 Runnable 接口子类对象。
3. 提交 Runnable 接口子类对象。
4. 关闭线程池。
*/
public class MyThread4 implements Runnable {
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);//创建线程池对象
MyThread4 r = new MyThread4();//创建Runnable接口子类对象。
service.submit(r);//提交Runnable接口子类对象。
service.submit(r);
service.shutdown();//关闭线程池。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
3. 线程的生命周期
一个线程的生命周期包含 5 个状态:新建、就绪、运行、阻塞、死亡。
新建(New):当线程对象对创建后(new),即进入了新建状态。就绪(Runnable):当调用线程对象的 start() 方法后,线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待 CPU 调度执行,并不是说此线程立即就会执行;运行(Running):当 CPU 开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注意:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;阻塞(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:1.等待阻塞:运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态;
2.同步阻塞 – 线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 – 通过调用线程的 sleep() 或 join() 或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep() 状态超时、join() 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
死亡(Dead):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。
4. 线程的同步
当多个线程共同操作同一块共享数据时,很有可能引发线程安全问题,从而造成数据异常。(一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。)
// 经典案例:三个窗口共同卖10张票。
可能出现两个问题:
- 三个窗口卖出同一张票(比如都卖出了第10号票)
- 卖出不应该存在的票(比如出现了第-1票)
1 | public class SellTickets implements Runnable { |
解决此类问题的思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。但是由于操作同步代码时,只能一个线程参与,其他线程等待。相当于是一个单线程的过程,效率比较低。
实现线程同步的方式:
方式一:同步代码块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/*
同步代码块:在方法中的某个区块中使用synchronized关键字,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
//需要同步操作的代码
}
注意:
① 任意对象都可以作为同步锁,但是多个线程要使用同一把锁【重要】。
② 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着 (BLOCKED),当线程执行完对应代码块/代码块发生异常后自动释放锁。
③ 同步代码块的锁除了自己指定外,很多时候也可以指定为this或类名.class
④ 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
⑤ 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
⑥ 需要同步操作的代码范围不能太大,也不能太小。
*/
public class SellTickets implements Runnable {
int tickets = 10;
public void run() {
while (true) {
synchronized (this) { // 同步代码块
if (tickets > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
System.out.println(Thread.currentThread().getName() +":卖第" + tickets + "张票");
tickets--;
}else{
break;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
SellTickets s = new SellTickets();
Thread t1 = new Thread(s,"窗口1"); // 新建线程1
Thread t2 = new Thread(s,"窗口2"); // 新建线程2
Thread t3 = new Thread(s,"窗口3"); // 新建线程3
t1.start(); // 启动线程1
t2.start(); // 启动线程2
t3.start(); // 启动线程3
}
}
结果如下(每次结果都可能不同):
窗口1:卖第10张票
窗口2:卖第9张票
窗口2:卖第8张票
窗口2:卖第7张票
窗口2:卖第6张票
窗口2:卖第5张票
窗口2:卖第4张票
窗口2:卖第3张票
窗口2:卖第2张票
窗口2:卖第1张票方式二:同步方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56/*
同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
//可能会产生线程安全问题的代码
}
注意:
① 对于非static方法,此时的同步锁就是this。
② 对于static方法,此时的同步锁是方法所在类的字节码对象(类名.class)。
③ 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
*/
public class SellTickets implements Runnable {
int tickets = 10;
public void run() {
while (tickets > 0) {
sell();
}
}
public synchronized void sell() { // 同步方法
if (tickets > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
System.out.println(Thread.currentThread().getName() + ":卖第" + tickets + "张票");
tickets--;
}
}
}
public class Test {
public static void main(String[] args) {
SellTickets s = new SellTickets();
Thread t1 = new Thread(s,"窗口1"); // 新建线程1
Thread t2 = new Thread(s,"窗口2"); // 新建线程2
Thread t3 = new Thread(s,"窗口3"); // 新建线程3
t1.start(); // 启动线程1
t2.start(); // 启动线程2
t3.start(); // 启动线程3
}
}
结果如下(每次结果都可能不同):
窗口1:卖第10张票
窗口1:卖第9张票
窗口1:卖第8张票
窗口1:卖第7张票
窗口1:卖第6张票
窗口1:卖第5张票
窗口1:卖第4张票
窗口1:卖第3张票
窗口1:卖第2张票
窗口1:卖第1张票方式三:Lock 锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69/*
Lock锁机制:通过创建Lock对象,采用lock()加锁,unlock()解锁,来保护指定的代码块
格式:
//类中
Lock 对象名 = new ReentrantLock();//创建对象
//run方法中
对象名.lock();//加同步锁
try{
//同步代码块
}catch(异常类型 e){
//异常处理
}finally{
对象名.unlock();//释放同步锁
}
注意:
① Lock实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作,优先使用Lock锁。
② java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
③ ReentrantLock类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
④ 锁定和取消锁定出现在不同作用范围中时,最好用try-【catch】-finally 加以保护,以确保在必要时释放锁。
*/
public class SellTickets implements Runnable {
int tickets = 10;
Lock lock =new ReentrantLock();
public void run() {
while (true) {
lock.lock();// 加同步锁
try {
if (tickets > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} // 为了更明显的突出线程安全问题,使用sleep()让线程执行慢一些
System.out.println(Thread.currentThread().getName() +":卖第" + tickets + "张票");
tickets--;
}else{
break;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();// 释放同步锁
}
}
}
}
public class Test {
public static void main(String[] args) {
SellTickets s = new SellTickets();
Thread t1 = new Thread(s,"窗口1"); // 新建线程1
Thread t2 = new Thread(s,"窗口2"); // 新建线程2
Thread t3 = new Thread(s,"窗口3"); // 新建线程3
t1.start(); // 启动线程1
t2.start(); // 启动线程2
t3.start(); // 启动线程3
}
}
结果如下(每次结果都可能不同):
窗口1:卖第10张票
窗口1:卖第9张票
窗口1:卖第8张票
窗口1:卖第7张票
窗口1:卖第6张票
窗口1:卖第5张票
窗口1:卖第4张票
窗口1:卖第3张票
窗口1:卖第2张票
窗口1:卖第1张票
★ Synchronized 和 Lock 的对比:
- Lock 是显式锁(手动开启和关闭锁),synchronized 是隐式锁,出了作用域自动释放锁。
- Lock 只有代码块锁,synchronized 有代码块锁和方法锁
- 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
5. 线程的通信
多个线程在处理同一个资源时,处理的动作(线程的任务)不相同,线程之间进行通信用来保证线程协调运行,比如控制线程执行先后顺序、获取某个线程执行的结果等
等待唤醒机制:在一个线程进行了规定操作后,就进入等待状态, 等待其他线程执行完他们的指定代码过后 再将其唤醒。
wait():令当前线程挂起,放弃CPU同步资源并等待(此时不会去竞争锁了,处于阻塞状态),使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用 notify() 或 notifyAll() 方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。notify():唤醒正在排队等待同步资源的线程,如果有多个会任意选择一个唤醒。notifyAll ():唤醒正在排队等待资源的所有线程结束等待。
注意:
① wait 方法与 notify 方法必须由同一个锁对象调用。只有对应的锁对象可以通过 notify 唤醒使用同一个锁对象调用 wait 方法后的线程。
② wait 方法与 notify 方法是属于 Object 类的方法的。
③ wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用,否则会报异常。因为:必须要通过锁对象调用这2个方法。
// 经典案例:生产者消费者问题(Producer-consumer problem)
生产者负责生产产品,而消费者负责消费产品。两个线程同时运行,要求产品的个数要大于0个,并小于10个。
如果产品个数为0,那么消费者需要等待生产者进行生产(只要生产了1个,消费者就可以继续进行消费)。
如果产品个数为10,那么生产者需要等待消费者进行消费(只要消费了1个,生产者就可以继续进行生产)。
1 | // 产品数量 |
1 | // 生产者 |
1 | // 消费者 |
1 | // 测试类 |
★sleep() 和 wait()的异同
相同点:一旦执行,都可以使得当前的线程进入阻塞状态。
不同点:
1)两个方法声明的位置不同:Thread 类中声明 sleep() , Object 类中声明 wait()
2)调用的要求不同:sleep() 可以在任何需要的场景下调用。 wait() 必须使用在同步代码块或同步方法中
3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep() 不会释放锁,wait() 会释放锁。
八. 枚举
1. 自定义枚举类
1 | /* |
2. enum关键字
1 | /* |
九. 注解
Annotation其实就是代码里的特殊标记,这些标记可以在编译, 类加载, 运行时被读取, 并执行相应的处理。通过使用 Annotation, 程序员可以在不改变原有逻辑的情况下, 在源文件中嵌入一些补充信息。注解类同于标签,标签为了解释事物,注解为了解释代码。(如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。)
1. 自定义注解
1 | /* |
2. Java 预置的注解
@Override::限定重写父类方法, 该注解只能用于方法@Deprecated: 用于表示所修饰的元素(类, 方法等)已过时。通常是因为所修饰的结构危险或存在更好的择@SuppressWarnings: 抑制编译器警告
3. 元注解
元注解可以理解为注解的注解。有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。
@Retention 当 @Retention 应用到一个注解上的时候,它指定了所修饰的 Annotation 的生命周期,默认为CLASS。取值如下:
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
对于自定义注解,如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现目的,所以自定义注解中肯定是使用 **@Retention(RetentionPolicy.RUNTIME)**。
@Documented 用于指定被该元 Annotation 修饰的 Annotation 类将被javadoc 工具提取成文档。默认情况下,javadoc是不包括注解的。
@Target 用于指定被修饰的 Annotation 能用于修饰哪些程序元素。取值如下:
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited 如果父类被 @Inherited 注解过的注解进行了注解,那么当其子类没有被任何注解应用,这个子类就继承了超类的注解。
@Repeatable 被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。
十. 集合
1. 集合概述
集合:java 中提供的一种容器,可以用来存储多个数据,所有集合类都位于 java.util 包下。
集合和数组的区别:
- 数组的长度不可变,而集合的长度是可变的。
- 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。
- 数组进行插入和删除操作较为麻烦,而集合很方便。
集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map
Collection 接口:单列数据,定义了存取一组对象的方法的集合,它有两个重要的子接口:
- List:元素有序且可重复的集合,主要实现类有 ArrayList 、LinkedList 和 Vector 。
- Set: 元素无序且不可重复的集合,主要实现类有 HashSet 、LinkedHashSet 和 TreeSet 。
Map 接口:双列数据,保存具有映射关系“key-value”的集合,主要实现类有 HashMap,LinkedHashMap,TreeMap。
2. Collection 接口
Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合。
JDK 不提供 Collection 接口的任何直接实现,而是提供更具体的子接口(如:Set和List)实现。
Collection 是所有单列集合的父接口,因此在 Collection 中定义了单列集合( List 和 Set )通用的一些方法,这些方法可用于操作所有的单列集合,常用方法如下:
boolean add(E e): 把给定的对象添加到当前集合中 。boolean addAll(Collection<? extends E> c):将指定 collection 中的所有元素都添加到此 collection 中)。void clear():清空集合中所有的元素。boolean remove(E e): 把给定的对象在当前集合中删除。boolean removeAll(Collection<?> c):移除此 collection 中那些也包含在指定 collection 中的所有元素(差集)。boolean contains(E e): 通过元素的 equals 方法判断当前集合中是否包含给定的对象。boolean isEmpty(): 判断当前集合是否为空。int size(): 返回集合中有效元素的个数。Object[] toArray(): 把集合中的元素,存储到数组中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class Test {
public static void main(String[] args) {
Collection collection = new ArrayList();
//增
collection.add("aaa");
collection.add("bbb");
collection.add("ccc");
System.out.println(collection); //[aaa, bbb, ccc]
//删
collection.remove("bbb");
System.out.println(collection);//[aaa, ccc]
//长度
System.out.println(collection.size());//2
//清空
collection.clear();
System.out.println(collection);//[]
}
}
Collection 集合的遍历
方式一:Iterator 迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/*
说明:
① Iterator对象称为迭代器(设计模式的一种),主要用于遍历 Collection 集合中的元素。
② Collection接口继承了java.lang.Iterable接口,该接口有一个iterator()方法,那么所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象。
③ Iterator仅用于遍历集合,本身并不提供承装对象的能力。如果需要创建Iterator对象,则必须有一个被迭代的集合。
④ 集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。
⑤ 迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
⑥ Iterator接口的常用方法如下:
public E next():返回迭代的下一个元素。
public boolean hasNext():如果仍有元素可以迭代,则返回 true。
注意:在进行集合元素取出时,如果集合中已经没有元素了,还继续使用迭代器的next方法,则会发生异常。
格式:
Iterator 对象名 = 集合名.iterator();
while(对象名.hasnext()){
System.out.println(iterator.next());
}
*/
public class Test {
public static void main(String[] args) {
Collection collection = new ArrayList(); //使用多态方式 创建对象
collection.add("aaa"); // 添加元素到集合
collection.add("bbb");
collection.add("ccc");
collection.add("ddd");
Iterator iterator = collection.iterator(); //创建迭代器对象
while(iterator.hasNext()){ //判断是否有迭代元素
System.out.println(iterator.next()); //输出迭代出的元素
}
}
}
结果为:
aaa
bbb
ccc
ddd方式二:增强 for 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/*
说明:
① 增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
② 遍历操作不需获取Collection或数组的长度,无需使用索引访问元素。
格式:
for(元素的数据类型 变量名 : Collection集合or数组){
System.out.println(变量名);
}
*/
public class Test {
public static void main(String[] args) {
Collection collection = new ArrayList(); //使用多态方式 创建对象
collection.add("aaa"); // 添加元素到集合
collection.add("bbb");
collection.add("ccc");
collection.add("ddd");
for (Object obj : collection){
System.out.println(obj);
}
}
}
3. List 接口
(1)List 接口概述
List 接口继承自 Collection 接口,是单列集合的一个重要分支,习惯性地会将实现了 List 接口的对象称为 List 集合。List 作为 Collection集合的子接口,不但继承了 Collection 接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法。
List接口特点:
- 元素存取有序。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
- 带有索引,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
- 集合中可以有重复的元素,通过元素的 equals 方法,来比较是否为重复的元素。
(2)List 接口常用方法
1 | 增: |
(3)List接口实现类
ArrayList:作为 List 接口的主要实现类;线程不安全的,效率高;底层使用 Object[] elementData 存储1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/*
ArrayList类的功能:用于实现长度可变的数组。
ArrayList类的使用:
①导包
import java.util.ArrayList;
②创建
ArrayList<引用数据类型> 对象名 = new ArrayList<>();//构造一个初始容量为10的空列表。
ArrayList<引用数据类型> 对象名 = new ArrayList<>(int initialCapacity);//构造有指定初始容量的空列表。
③使用
增删改查长度遍历
ArrayList类的注意事项:
①对于ArrayList来说,有一个尖括号<E>代表泛型,E取自Element(元素)的首字母。使用一种引用数据类型将E其替换即可。
②泛型:也就是装在集合当中的所有元素,全都是统一的什么类型。(泛型只能是引用类型,不能是基本类型。)
③对于ArrayList集合来说,直接打印得到的不是地址值,而是内容。如果内容是空,得到的是空的中括号:[]
④对于ArrayList集合来说,add添加动作一定是成功的,所以返回值可用可不用。但是对于其他集合来说,add添加动作不一定成功。
⑤如果希望向集合ArrayList当中存储基本类型数据,必须使用基本类型对应的“包装类”。
⑥ArrayList元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。
*/
//示例:生成3个范围为[1,10]的随机数字,添加到ArrayList集合中,并遍历输出。
import java.util.Random;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
Random random = new Random();
ArrayList<Integer> list = new ArrayList<>();
for (int i = 0; i < 3; i++) {
int num = random.nextInt(10) + 1;
list.add(num);
}
System.out.print("集合中的元素为:");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
}
}LinkedList:对于频繁的插入、删除操作,使用此类效率比 ArrayList 高;底层使用双向链表存储。1
2
3
4
5
6
7// 新增方法:
void addFirst(E e):将指定元素插入此列表的开头。
void addLast(E e):将指定元素添加到此列表的结尾。
E getFirst():返回此列表的第一个元素。
E getLast():返回此列表的最后一个元素。
E removeFirst():移除并返回此列表的第一个元素。
E removeLast():移除并返回此列表的最后一个元素。Vector:作为 List 接口的古老实现类;线程安全的,效率低;底层使用 Object[] elementData 存储与 ArrayList 相似,但是 Vector 是同步的。所以说 Vector 是线程安全的动态数组。它的操作与 ArrayList 几乎一样。
★ ArrayList 和 LinkedList 的异同
二者都线程不安全,相对线程安全的 Vector,执行效率高。此外,ArrayList 是实现了基于动态数组的数据结构。LinkedList 基于链表的数据结构。对于随机访问 get 和 set,ArrayList 优于 LinkedList,因为 LinkedList 要移动针。对于新增和删除操作 add(特指插入)和 remove,LinkedList 比较占优势,因为 ArrayList 要移动数据。
4. Set 接口
(1)Set 接口概述
Set 接口和 List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是,Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set 接口特点:
- 元素存取无序。不等于随机性。存储的数据在底层数组中并非照数组索引的顺序添加,而是根据数据的哈希值决定的。
- 集合中不可以有重复的元素。保证添加的元素照 equals() 判断时,不能返回 true。
(2)Set接口实现类
HashSet:作为 Set 接口的主要实现类;线程不安全的;可以存储 null 值。java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持。
HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性的方式依赖于:hashCode与equals方法。两个对象通过 hashCode() 方法比较相等,并且两个对象的 equals() 方法返回值也相等。
对于存放在 Set 容器中的对象,对应的类一定要重写 equals() 和 hashCode(Object obj) 方法,以实现对象相等规则。
向HashSet中添加元素的过程
- 当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode()方法来得到该对象的hashCode 值,然后根据 hashCode 值,通过某种散列函数决定该对象在 HashSet 底层数组中的存储位置。(这个散列函数会与底层数组的长度相计算得到在数组中的下标,并且这种散列函数计算还尽可能保证能均匀存储元素,越是散列分布,该散列函数设计的越好)
- 如果两个元素的 hashCode 值相等,会再继续调用 equals 方法,如果 equals 方法结果为 true,添加失败;如果为 false,那么会保存该元素,若该数组的位置已经有元素了,那么会通过链表的方式继续链接。
- 如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置,但依然可以添加成功【因此当重写了 equals 后一定要重写 hashCode】。
重写 hashCode() 方法的基本原则
- 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。
- 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode()方法的返回值也应相等。
- 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
重写 equals() 方法的基本原则
当一个类有自己特有的“逻辑相等”概念,当改写 equals() 的时候,总是要改写 hashCode(),根据一个类的equals 方法(改写后),两个截然不同的实例有可能在逻辑上是相等的,但是,根据 Object.hashCode() 方法,它们仅仅是两个对象。因此,违反了“相等的对象必须具有相等的散列码”。
结论:重写 equals 方法的时候一般都需要同时复写 hashCode方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals() 中进行计算。
HashSet集合存储数据的结构(哈希表)
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一 hash 值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过 key 值依次查找的效率较低。而 JDK1.8 中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public class Student { //自定义学生类
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
//重写hashcode方法
public int hashCode() {
return Objects.hash(name, age);
}
//重写toString方法
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add(new Student("张三",18));
hashSet.add(new Student("李四",20));
hashSet.add(new Student("王二",24));
hashSet.add(new Student("李四",20));
for (Object obj: hashSet){ //遍历
System.out.println(obj);
}
}
}
遍历结果为://不可重复,不按插入顺序输出
Student{name='王二', age=24}
Student{name='张三', age=18}
Student{name='李四', age=20}LinkedHashSet:作为 HashSet 的子类;遍历其内部数据时,可以按照添加的顺序遍历。- LinkedHashSet 是 HashSet 的子类。
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的,在遍历时会按照插入顺序输出。
- LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很好的性能。
- LinkedHashSet 不允许集合元素重复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54public class Student { //自定义学生类
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重写equals方法
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
//重写hashcode方法
public int hashCode() {
return Objects.hash(name, age);
}
//重写toString方法
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Student("张三",18));
linkedHashSet.add(new Student("李四",20));
linkedHashSet.add(new Student("王二",24));
linkedHashSet.add(new Student("李四",20));
Iterator iterator = linkedHashSet.iterator();
while(iterator.hasNext()){ //遍历
System.out.println(iterator.next());
}
}
}
遍历结果为://不可重复,按插入顺序输出
Student{name='张三', age=18}
Student{name='李四', age=20}
Student{name='王二', age=24}TreeSet:可以照添加对象的指定属性,进行排序。- TreeSet 是 SortedSet 接口的实现类,TreeSet 可以确保集合元素处于排序状态。
- TreeSet 底层使用 红黑树结构存储数据。
- TreeSet 支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。当我们构造 TreeSet时,若使用不带参数的构造函数,则 TreeSet 的使用自然比较器;若用户需要使用自定义的比较器,则需要使用带比较器的参数。
- 注意 TreeSet 集合不是通过 hashcode 和 equals 函数来比较元素的。它是通过 compare 或者 comparaeTo 函数来判断元素是否相等。compare 函数通过判断两个对象的 id,相同的 id 判断为重复元素,不会被加入到集合中。
- 向 TreeSet 中添加的数据,要求是相同类的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58// 自然排序:TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素的大小,然后将集合元素按升序(默认情况)排列。
public class Student implements Comparable {
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重写toString方法
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public int compareTo(Object o) { //先按照学生的age进行排序,如果age相同,再按照姓名自然排序
if(o instanceof Student){
Student s = (Student) o;
if (this.age > s.age){
return 1;
}else if (this.age < s.age){
return -1;
}else{
return this.name.compareTo(s.name);
}
}else{
throw new RuntimeException("比较类型错误!");
}
}
}
public class Main {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add(new Student("zhangsan",24));
treeSet.add(new Student("lisi",15));
treeSet.add(new Student("wanger",20));
treeSet.add(new Student("lisi",15));
treeSet.add(new Student("lisi",24));
Iterator iterator = treeSet.iterator();
while(iterator.hasNext()){ //遍历
System.out.println(iterator.next());
}
}
}
结果如下:
Student{name='lisi', age=15}
Student{name='wanger', age=20}
Student{name='lisi', age=24}
Student{name='zhangsan', age=24}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61// 定制排序,要实现定制排序,需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。
public class Student {
String name;
int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
//重写toString方法
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class Main {
public static void main(String[] args) {
Comparator comparator = new Comparator() {
public int compare(Object o1, Object o2) { //先按学生的age排序,如果age相同,再按照姓名自然排序
if(o1 instanceof Student && o2 instanceof Student){
Student s1 = (Student) o1;
Student s2 = (Student) o2;
if (s1.age > s2.age){
return 1;
}else if (s1.age < s2.age){
return -1;
}else{
return s1.name.compareTo(s2.name);
}
}else{
throw new RuntimeException("比较类型错误!");
}
}
};
TreeSet treeSet = new TreeSet(comparator); //定制排序
treeSet.add(new Student("zhangsan",24));
treeSet.add(new Student("lisi",15));
treeSet.add(new Student("wanger",20));
treeSet.add(new Student("lisi",15));
treeSet.add(new Student("lisi",24));
Iterator iterator = treeSet.iterator();
while(iterator.hasNext()){ //遍历
System.out.println(iterator.next());
}
}
}
结果如下:
Student{name='lisi', age=15}
Student{name='wanger', age=20}
Student{name='lisi', age=24}
Student{name='zhangsan', age=24}
5. Map 接口
(1)Map 接口概述
Map 与 Collection 并列存在。用于保存具有映射关系的数据:key-value。
List 接口特点:
- key 和 value 都可以是任何引用类型的数据
- key 用 Set 来存放, 不允许重复,即同一个 Map 对象所对应的类,必须重写hashCode()和equals()方法。
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value。
(2)Map接口常用方法
1 | 增: |
(3)Map接口实现类
HashMap:作为 Map 的主要实现类;线程不安全的,效率高;可以存储 null 的 key 和 value允许使用 null 键和 null 值,与 HashSet 一样,不保证映射的顺序。
所有的 key 构成的集合是 Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()。
所有的 value 构成的集合是 Collection:无序的、可以重复的。所以,value 所在的类要重写:equals()。
一个 key-value 构成一个 entry,所有的 ntry构成的集合是 Set:无序的、不可重复的
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。
HashMap 在 jdk7 中实现原理:
HashMap map = new HashMap(); 在实例化以后,底层创建了长度是 16 的一维数组 Entry[] table
map.put(key1,value1);
首先:调用 key1 所在类的
hashCode()计算 key1 哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
- 如果此位置上的数据为空,此时的 key1-value1 添加成功。 【情况1】
- 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较 key1和已经存在的一个或多个数据的哈希值:
- 如果key1 的哈希值与已经存在的数据的哈希值都不相同,此时 key1-value1 添加成功。【情况2】
- 如果key1 的哈希值和已经存在的某一个数据 (key2-value2) 的哈希值相同,继续比较:调用 key1 所在类
equals(key2)方法:- 如果equals() 返回false:此时key1-value1添加成功。【情况3】
- 如果equals() 返回 true:使用 value1 替换 value2。【情况】
在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来。
HashMap 在 jdk8 中相较于 jdk7 在底层实现方面的不同
- new HashMap(); 底层默认创建一个长度为16的数组
- 首次调用put()方法时,底层创建长度为16的数组,jdk8底层的数组是:Node[],而非Entry[]
- jdk7 底层结构:数组+链表。jdk8 中底层结构:数组+链表+红黑树。
- 形成链表时(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
- 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
HashMap底层典型属性的属性的说明
- DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
- DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
- threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
- TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
- MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
`LinkedHashMap`:LinkedHashMap 是 HashMap 的子类。在 HashMap 存储结构的基础上,使用了一对双向链表来记录添加元素的顺序,与 LinkedHashSet 类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致。`TreeMap`: 保证照添加的 key-value 对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序,底层使用红黑树。 - 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException。 - 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口。 - TreeMap判断 两个key 相等的标准:两个key通过compareTo()方法或者compare()方法返回0。`Hashtable`:作为古老的实现类;线程安全的,效率低;不能存储 null 的 key 和 value,Hashtable 实现原理、功能和 HashMap 相同。
(4)Collections 工具类
Collections 是一个操作 Set、List 和 Map 等集合的工具类,就像 Arrays 是操作数组的工具类一样。Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法,常用方法如下:
static <T> boolean addAll(Collection<T> c, T... elements):将所有指定元素添加到指定 collection 中。static void shuffle(List<?> list):对 List 集合元素进行随机排序。static <T> void sort(List<T> list):根据元素的自然顺序对指定列表按升序进行排序。static <T> void sort(List<T> list,Comparator<? super T> ):根据指定比较器产生的顺序对指定列表进行排序。static void swap(List<?> list, int i, int j):在指定列表的指定位置处交换元素static void reverse(List<?> list):反转指定列表中元素的顺序static Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素static Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素。Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。ArrayList和HashMap都是线程不安全的,如果程序要求线程安全,我们可以将ArrayList、HashMap转换为线程安全的。使用
synchronizedList(List list)和synchronizedMap(Map map)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class Main {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add(2);
arrayList.add(3);
arrayList.add(1);
System.out.println(arrayList);//[2, 3, 1]
// 反转
Collections.reverse(arrayList);
System.out.println(arrayList);//[1, 3, 2]
// 排序
Collections.sort(arrayList);
System.out.println(arrayList);//[1, 2, 3]
// 获取最大小值
System.out.println(Collections.max(arrayList));//3
System.out.println(Collections.min(arrayList));//1
// 随机排序
Collections.shuffle(arrayList);
System.out.println(arrayList);//[3, 1, 2]
// 交换
Collections.swap(arrayList,0,1);
System.out.println(arrayList);//[1, 3, 2]
}
}
十一. 泛型
1. 泛型的概念
泛型,即“参数化类型”,把类型当作是参数一样传递。就是允许在定义类、接口时通过一个标识表示类中某个属性的类型或者是某个方法的返回值及参数类型。这个类型参数将在使用时(例如,继承或实现这个接口,用这个类型声明变量、创建对象时)确定(即传入实际的类型参数,也称为类型实参)。
2. 泛型的好处
- 泛型将代码安全性检查提前到编译期(程序更加健壮)
- 泛型能够省去类型强制转换(代码更加简洁)
- 在编写的时候,就限定了类型(可读性和稳定性更好)
1 | // 定义一个集合,不使用泛型 |
3. 泛型的定义与使用
(1)泛型类的定义与使用
1 | /*泛型类型用于类的定义中,被称为泛型类,用户使用该类的时候,才把类型明确下来。 |
(2)泛型接口的定义与使用
1 | /*泛型类型用于接口的定义中,被称为泛型接口。 |
(3)泛型方法的定义与使用
1 | /*泛型类型用于方法的定义中,被称为泛型方法。 |
4. 泛型通配符
当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用 Object 类中的共性方法,集合中元素自身方法无法使用。
无界通配符:类型名称 <?> 对象名称 (可以接收任何类型)
上界通配符:类型名称 <? extends 类 > 对象名称 (只能接收该类型及其子类)
下界通配符: 类型名称 <? super 类 > 对象名称 (只能接收该类型及其父类型)
1 | public class Main { |
十二. IO 流
1. File 类的使用
1 | /* |
2. IO 概述
(1)基本概念
I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。Java程序中,对于数据的输入/输出操作以 “流(stream)” ” 的方式进行。
流代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。
(2)流的分类
按数据的流向分为:输入流和输出流。
输入流 :把数据从
其他设备上读取到内存中的流。输出流 :把数据从
内存中写出到其他设备上的流。
按处理数据单位不同分为:字节流和字符流。
字节流 :以字节为单位,读写数据的流(用于操作图片、视频等非文本文件,建议不要操作文本文件,可能会乱码)。
字符流 :以字符为单位,读写数据的流(只能操作文本文件,不能操作图片,视频等非文本文件。)。
按功能不同分为:节点流、处理流
节点流:以从或向一个特定的地方(节点)读写数据。如 FileInputStream
处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装。
所有的流都继承下面四个抽象基类,且子类名称是以其父类名作为子类名后缀。
| 输入流 | 输出流 | |
|---|---|---|
| 字节流 | InputStream | OutputStream |
| 字符流 | Reader | Writer |
IO流体系
| 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 | |
|---|---|---|---|---|
| 抽象基类 | InputStream |
OutputStream |
Reader |
Writer |
| 访问文件 | FileInputStream |
FileOutputStream |
FileReader |
FileWriter |
| 访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
| 访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
| 缓冲流 | BufferedInputStream |
BufferedOutputStream |
BufferedReader |
BufferedWriter |
| 转换流 | InputStreamReader |
InputStreamWriter |
||
| 对象流 | ObjectInputStream |
ObjectOutputStream |
||
| FilterInputStream | FilterOutputStream | FilterReader | FilterWriter | |
| 打印流 | PrintStream | PrintWriter | ||
| 特殊流 | DataInputStream | DataOutputStream |
3. 抽象基类
(1)InputStream
java.io.InputStream 类是字节输入流所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。
void close():关闭此输入流并释放与此流相关联的任何系统资源。(程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应显式关闭文件 IO 资源。)abstract int read():从输入流读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。int read(byte[] b):从输入流中读取最多 b.length 一些字节数,并将它们存储到字节数组 b 中。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。否则以整数形式返回实际读取的字节数。int read(byte[] b, int off, int len):将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。如果因为流位于文件末尾而没有可用的字节,则返回值 -1
(2)OutputStream
java.io.OutputStream 类是字节输出流所有类的超类,将指定字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。
void close():关闭此输出流并释放与此流相关联的任何系统资源。void flush():刷新此输出流并强制任何缓冲的输出字节被写出。void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。void write(byte[] b, int off, int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。abstract void write(int b):将指定的字节写入此输出流。
(3)Reader
java.io.Reader 类是用于读取字符流的所有类的超类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。
void close():关闭此流并释放与此流相关联的任何系统资源。int read(): 从输入流读取一个字符,自动提升为int类型。 范围在 0 到 65535 之间 ,如果已到达流的末尾,则返回 -1int read(char[] cbuf): 将字符读入数组。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。int read(char[] cbuf,int off,int len): 将字符读入数组的某一部分。存到数组cbuf中,从off处开始存储,最多读len个字符。如果已到达流的末尾,则返回 -1。否则返回本次读取的字符数。
(4)Writer
java.io.Writer 类是用于写出字符流的所有类的超类,将指定字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。
void write(int c): 写入单个字符。void write(char[] cbuf): 写入字符数组。abstract void write(char[] cbuf, int off, int len): 写入字符数组的某一部分,off为开始索引,len为字符个数。 入len个字符部分到文件写入器, 从偏移量 off 的位置读取字符数组。void write(String str): 写入字符串。void write(String str, int off, int len): 写入字符串的某一部分,off字符串的开始索引,len写的字符个数。void flush(): 刷新该流的缓冲。void close(): 关闭此流,但要先刷新它。
4. 文件流
(1)FileReader类
1 | /* |
(2)FileWriter类
1 | /* |
(3)FileInputStream类
1 | /* |
(4)FileOutputStream类
1 | /* |
5. 缓冲流
使用缓冲流的好处是,能够高效的读写信息,原理是在创建流对象时,会创建一个内置的默认大小(8Kb)的缓冲区数组,通过缓冲区读写,减少系统IO次数,从而提高读写的效率。
- 当读取数据时,数据按块读入缓冲区,其后的读操作则直接访问缓冲区。
- 当使用 BufferedInputStream 读取字节文件时,BufferedInputStream 会一次性从文件中读取 8192个(8Kb),存在缓冲区中,直到缓冲区装满了,才重新从文件中读取下一个 8192 个字节数组。
- 向流中写入字节时,不会直接写到文件,先写到缓冲区中直到缓冲区写满,BufferedOutputStream 才会把缓冲区中的数据一次性写到文件里。使用方法 flush() 可以强制将缓冲区的内容全部写入输出流。
- 关闭流的顺序和打开流的顺序相反。只要关闭最外层流即可,关闭最外层流也会相应关闭内层节点流。
- flush() 方法的使用:手动将buffer中内容写入文件。
- 如果是带缓冲区的流对象的 close() 方法,不但会关闭流,还会在关闭流之前刷新缓冲区,关闭后不能再写出。
(1)字节缓冲流
1 | /* |
(2)字符缓冲流
1 | /* |
6. 转换流
计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码 。比如说,按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。
编码:字符(能看懂的)–字节(看不懂的)
解码:字节(看不懂的)–>字符(能看懂的)
- 转换流提供了在字节流和字符流之间的转换
- Java API提供了两个转换流:
- InputStreamReader :将InputStream 转换为Reader(字节到字符的桥梁)
- OutputStreamWriter :将Writer 转换为OutputStream(字符到字节的桥梁)
- 字节流中的数据都是字符时,转成字符流操作更高效。
- 很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。
(1)InputStreamReader 类
转换流java.io.InputStreamReader,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。
在 IDEA 中,使用FileReader 读取项目中的文本文件。由于 IDEA 的设置,都是默认的UTF-8编码,所以没有任何问题。但是,当读取 Windows 系统中创建的文本文件时,由于 Windows 系统的默认是 GBK 编码,就会出现乱码。可以通过转换流读取 GBK 编码文件。
1 | /* |
(2)OutputStreamWriter 类
转换流java.io.OutputStreamWriter ,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。
1 | /* |
7. 对象流(序列化流)
Java 提供了一种对象序列化的机制。用一个字节序列可以表示一个对象,该字节序列包含该对象的数据、对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
反之,该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。
ObjectOutputStream:内存中的对象—>存储中的文件、通过网络传输出去:序列化过程
ObjectInputStream:存储中的文件、通过网络接收过来 —>内存中的对象:反序列化过程
对象的序列化机制:
对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
(1)ObjectOutputStream 类
java.io.ObjectOutputStream 类,将 Java 对象的原始数据类型写出到文件,实现对象的持久存储。
1 | /* |
(2)ObjectInputStream类
ObjectInputStream 反序列化流,将之前使用 ObjectOutputStream 序列化的原始数据恢复为对象。
1 | /* |
十三. 网络编程
1. IP 地址和端口号
(1)IP 地址
IP地址:指互联网协议地址(Internet Protocol Address),俗称IP。IP地址用来给一个网络中的计算机设备做唯一的编号。
IP 地址分类
IPv4:是一个 32 位的二进制数,通常被分为 4 个字节,表示成
a.b.c.d的形式,例如192.168.65.100。其中 a、b、c、d 都是 0~255 之间的十进制整数,那么最多可以表示 42 亿个。IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得 IP 的分配越发紧张。
为了扩大地址空间,拟通过 IPv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分成 8 组十六进制数,表示成
ABCD:EF01:2345:6789:ABCD:EF01:2345:6789,号称可以为全世界的每一粒沙子编上一个网址,这样就解决了网络地址资源数量不够的问题。
特殊的 IP 地址
- 本机IP地址:
127.0.0.1、localhost。
(2)端口号
网络的通信,本质上是两个进程(应用程序)的通信。每台计算机都有很多的进程,那么在网络通信时,如何区分这些进程呢?
如果说 IP 地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的进程(应用程序)了。 端口与 号与IP 地址的组合得出一个网络套接字:Socket。
端口号标识正在计算机上运行的进程(程序),用两个字节表示的整数,它的取值范围是 0~65535。
端口分类:
- 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP 占用端口 80,FTP 占用端口 21,Telnet 占用端口23)
- 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat 占用端口 8080,MySQL 占用端口 3306)
- 动态/ 私有端口:49152~65535。
(3)InetAddress 类
InetAddress 类:此类的一个对象就代表着一个具体的 IP 地址,拥有两个子类:Inet4Address、Inet6Address。
1 | // InetAddress类没有提供公共的构造器,而是提供了如下几个静态方法来获取 InetAddress 实例 |
2. 网络协议
(1)TCP 协议和 UDP 协议
java.net 包中提供了两种常见的网络协议的支持:
UDP:用户数据报协议 (User Datagram Protocol)。UDP 是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输例如视频会议都使用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议。
特点:
- 数据被限制在 64kb 以内,超出这个范围就不能发送了。
- 可以广播发送
- 发送数据结束时无需释放资源,开销小,速度快
- 发送不管对方是否准备好,接收方收到也不确认,故是不可靠的
TCP:传输控制协议 (Transmission Control Protocol)。TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”。
特点:
- 在连接中可进行大数据量的传输
- 传输完毕,需释放已建立的连接,效率低
- TCP 协议进行通信的两个应用进程:客户端、服务端。
(2)TCP 通信
TCP 通信能实现两台计算机之间的数据交互,通信的两端,要严格区分为客户端(Client)与服务端(Server)。
两端通信时步骤:
- 服务端程序,需要事先启动,等待客户端的连接。
- 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。
在Java中,提供了两个类用于实现TCP通信程序:
- 客户端:
java.net.Socket类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。 - 服务端:
java.net.ServerSocket类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接
(3)Socket 类
Socket 类:该类实现客户端套接字,套接字指的是两台设备之间通讯的端点。
1 | /* |
(4)ServerSocket类
ServerSocket类:这个类实现了服务器套接字,该对象等待通过网络的请求。
1 | /* |
(5)简单的TCP通信实例
客户端 Socket 的工作过程包含以下四个基本的步骤 :
- 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。
- 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用getOutputStream()方法获得输出流,进行数据传输。
- 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息(但不能读取自己放入线路的信息),通过输出流将信息写入线程。
- 关闭 Socket:断开客户端到服务器的连接,释放线路
服务器程序的工作过程包含以下四个基本的步骤:
- 调用 ServerSocket(int port) : 创建一个服务器端套接字,并绑定到指定端口上。用于监听客户端的请求。
- 调用 accept(): 监听连接请求,如果客户端请求连接,则接受连接,返回通信套接字对象。
- 调用该 Socket 类对象的 getOutputStream() 和 和 getInputStream (): 获取输出流和输入流,开始网络数据的发送和接收。
- 关闭 ServerSocket 和 Socket 对象:客户端访问结束,关闭通信套接字。
★ TCP 通信分析流程
【服务端】启动,创建 ServerSocket 对象,等待连接。
【客户端】启动,创建 Socket 对象,请求连接。
【服务端】接收连接,调用 accept 方法,并返回一个 Socket 对象。
【客户端】Socket 对象,获取 OutputStream,向服务端写出数据。
【服务端】Scoket 对象,获取 InputStream,读取客户端发送的数据。
【服务端】Socket对象,获取OutputStream,向客户端回写数据。
【客户端】Scoket对象,获取InputStream,解析回写数据。
【客户端】释放资源,断开连接。
1 | public class Server { |
十四. 反射
Java 的反射(reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为 Java 语言的反射机制。反射被视为动态语言的关键。
1. Class 类的理解
类的加载过程:
程序经过 javac.exe 命令以后,会生成一个或多个字节码文件(.class结尾)。接着我们使用 java.exe 命令对某个字节码文件进行解释运行。相当于将某个字节码文件加载到内存中。此过程就称为类的加载。加载到内存中的类,我们就称为运行时类,此运行时类,就作为 Class 的一个实例。换句话说,Class 的实例就对应着一个运行时类。加载到内存中的运行时类,会缓存一定的时间。在此时间之内,我们可以通过不同的方式来获取此运行时类。
2. 获取反射中的 Class 对象
在 Java API 中,获取 Class 类对象有三种方法:
方法一:使用 Class.forName 静态方法:若已知一个类的全路径名(包名.类名),可通过 Class 类的静态方法forName() 获取,可能抛出异常。例如:Class clz = Class.forName(“java.lang.String”);
将字节码文件加载进内存,返回Class对象。多用于配置文件,将类名定义在配置文件中。读取文件,加载类。
方法二:使用 .class 方法。若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序性能最高。例如:Class clz = String.class;
多用于参数的传递
方法三:使用类对象的 getClass() 方法。若已知某个类的实例,调用该实例的 getClass() 方法获取 Class 对象。
例如:String str = new String(“Hello”); Class clz = str.getClass();getClass() 方法在 Object 类中定义着。多用于对象的获取字节码的方式
1 | public class Student { |
3. 创建运行时类的对象
通过反射创建类对象主要有两种方法:
方法一:通过 Class 对象的 newInstance() 方法。内部调用了运行时类的空参的构造器。要求:
① 运行时类必须提供空参的构造器
② 空参的构造器的访问权限得够。通常,设置为 public。方法二:通过 Constructor 对象的 newInstance() 方法。通过 Constructor 对象创建类对象可以选择特定构造方法。
1 | public class Main { |
4. 获取类属性、方法、构造器
(1)获取类属性
获取类的全部属性
使用 Class 对象的
public Field[] getFields()方法可以获取 Class 类的所有 public 属性,无法获取私有属性。使用 Class 对象的
public Field[] getDeclaredFields()方法则可以获取包括私有属性在内的所有属性。获取类的指定属性
public Field getField(String name)返回此 Class 对象表示的类或接口的指定的 public 的 Field。public Field getDeclaredField(String name)返回此 Class 对象表示的类或接口的指定的 Field。在反射机制中,可以直接通过 Field 类操作类中的属性,通过 Field 类提供的 set() 和 get() 方法就可以完成设置和取得属性内容的操作。
public Object get (Object obj)取得指定对象 obj 上此 Field 的属性内容public void set (Object obj,Object value)设置指定对象 obj 上此 Field 的属性内容。★注意:
对于私有属性,使用 get,set 时,先使用
Filed对象.setAccessible(true); 以保证当前属性是可访问的,如果不设置,则不能访问私有属性。- Method 和 Field、Constructor 对象都有 setAccessible() 方法。
- setAccessible 启动和禁用访问安全检查的开关。
- 参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
- 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调用,那么请设置为 true。使得原本无法访问的私有成员也可以访问
- 参数值为false则指示反射的对象应该实施Java语言访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Student.class;
Student student = (Student) clazz.newInstance();
// 获取类的全部公有属性
System.out.println("获取类的全部公有属性");
Field[] fields = clazz.getFields();
for(Field f :fields){
System.out.println(f);// public int reflectiontest.Student.age
}
// 获取类的全部属性
System.out.println("获取类的全部属性");
Field[] declaredFields = clazz.getDeclaredFields();
for(Field f:declaredFields){
System.out.println(f);// private java.lang.String reflectiontest.Student.name
// public int reflectiontest.Student.age
}
// 获取类的指定公有属性
System.out.println("获取类的指定公有属性");
Field age = clazz.getField("age");
System.out.println(age);// public int reflectiontest.Student.age
age.set(student,18);
System.out.println(age.get(student));// 18
// 获取类的指定任意属性
System.out.println("获取类的指定任意属性");
Field name = clazz.getDeclaredField("name");
System.out.println(name);// private java.lang.String reflectiontest.Student.name
name.setAccessible(true);// 暴力反射,保证当前属性是可访问的,如果不设置,则不能访问私有属性★
name.set(student,"张三");
System.out.println(name.get(student));// 张三
}
}
(2)获取类方法
获取类的全部方法
public Method[] getDeclaredMethods()返回此 Class 对象所表示的类或接口的全部方法。public Method[] getMethods()返回此 Class 对象所表示的类或接口的 public 的方法(包含其父类和父接口的 public 方法)。获取类的指定方法
通过反射,调用类中的方法,通过 Method 类完成。步骤:
使用
public Method getMethod(String name,Class…parameterTypes)返回此 Class 对象表示的类或接口的指定的 public 的 Method。参数1 :指明获取的方法的名称 参数2:指明获取的方法的形参列表或使用
public Method getDeclaredMethod(String name,Class…parameterTypes)返回此 Class 对象表示的类或接口的指定的 Method。之后使用
Object invoke (Object obj, Object[] args)进行调用,并向方法中传递要设置的 obj对象的参数信息说明:
① Object 对应原方法的返回值,若原方法无返回值,此时返回 null
② 若原方法若为静态方法,此时形参 Object obj 可为 null
③ 若原方法形参列表为空,则Object[] args为null
④ 若原方法声明为 private,则需要在调用此 invoke() 方法前,显式调用方法对象的 setAccessible(true)方法,才可访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Student.class;
Student student = (Student) clazz.newInstance();
// 获取类的全部公有方法
System.out.println("获取类的全部公有方法");
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);//
}
// 获取类的全部方法
System.out.println("获取类的全部方法");
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
// 获取类的指定公有方法
System.out.println("获取类的指定公有方法");
Method sleep = clazz.getMethod("sleep");
System.out.println(sleep);// public void reflectiontest.Student.sleep()
sleep.invoke(student);// 学生在睡觉
// 获取类的指定任意方法
System.out.println("获取类的指定任意方法");
Method study = clazz.getDeclaredMethod("study");
System.out.println(study);// private void reflectiontest.Student.study()
study.setAccessible(true);// 保证当前方法是可访问的,如果不设置,则不能访问私有方法★
study.invoke(student);// 学生在学习
}
}
(3)获取类构造器
获取类的全部构造器
public Constructor<T>[] getConstructors()返回此 Class 对象所表示的类的所有 public 构造方法。public Constructor<T>[] getDeclaredConstructors()返回此 Class 对象表示的类声明的所有构造方法。获取类的指定构造器
public Constructor<T> getConstructor(Class<?>... parameterTypes)返回一个 Constructor 对象,它反映此 Class 对象所表示的类的指定公共构造方法。参数指明构造器的参数列表public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes)返回一个 Constructor 对象,该对象反映此 Class 对象所表示的类或接口的指定构造方法。调用构造方法:
Constructor–>newInstance(Object… initargs)
newInstance是 Constructor类的方法(管理构造函数的类)
api 的解释为:newInstance(Object… initargs) ,使用此 Constructor 对象表示的构造方法来创建该构造方法的声明类的新实例,并用指定的初始化参数初始化该实例。
它的返回值是 T 类型,所以 newInstance 是创建了一个构造方法的声明类的新实例对象,并为之调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Student.class;
Student student = (Student) clazz.newInstance();
// 获取类的全部公有构造器
System.out.println("获取类的全部公有构造器");
Constructor[] constructors = clazz.getConstructors();
for (Constructor c : constructors) {
System.out.println(c);//
}
// 获取类的全部构造器
System.out.println("获取类的全部构造器");
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();
for (Constructor c : declaredConstructors) {
System.out.println(c);
}
// 获取类的指定公有构造器
System.out.println("获取类的指定公有构造器");
Constructor constructor = clazz.getConstructor(String.class,int.class);
System.out.println(constructor);// public reflectiontest.Student(java.lang.String,int)
Object o = constructor.newInstance("张三",18);
System.out.println(o);// Student{name='张三', age=18}
// 获取类的指定任意构造器
System.out.println("获取类的指定任意构造器");
Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
System.out.println(declaredConstructor);// public reflectiontest.Student(java.lang.String,int)
// declaredConstructor.setAccessible(true);// 保证当前方法是可访问的,如果不设置,则不能访问私有构造器★
Object o1 = declaredConstructor.newInstance("李四",20);
System.out.println(o1);// Student{name='李四', age=20}
}
}