Java 概述

命令行执行Java代码

1
2
3
4
# 将java文件编译成class文件
javac 文件名.java
# 找到主类名,执行代码
java 主类名

转义字符

通过 / 对后面的字符进行转义,以达到不同的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ChangeChar {
public static void main(String[] args) {
// \t: 一个制表位,实现对齐功能
System.out.println("北京\t天津\t上海");
// \n: 换行符
System.out.println("北京\n天津\n上海");
// \\: 一个\
System.out.println("C:\\code\\java");
// \": 一个"
System.out.println("老师说:\"认真学习\"");
// \': 一个'
System.out.println("老师说:\'认真学习\'");
// \r: 一个回车(回车与换行不同,回车代表光标会移动到当前项)
System.out.println("你好,世界\r哈喽");
}
}

输出结果:

注释

注释不会被Java虚拟机JVM编译,故而可以在代码上添加注释,方便他人和自己阅读

  1. 单行注释:// 注释内容
  2. 多行注释:/* 注释内容 */
  3. 文档注释:注释内容可以被JDK提供的工具 javadoc 所解析,生成一套以网页文件形式体现的该程序的说明文档,一般写在类的上面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文档注释,其中@author和@version是javadoc 的标签,可以用于生成说明文档
/**
* @author 奥数定理
* @version 1.0
* */
public class ChangeChar {
public static void main(String[] args) {
// \t: 一个制表位,实现对齐功能
System.out.println("北京\t天津\t上海");
// \n: 换行符
System.out.println("北京\n天津\n上海");
// \\: 一个\
System.out.println("C:\\code\\java");
// \": 一个"
System.out.println("老师说:\"认真学习\"");
// \': 一个'
System.out.println("老师说:\'认真学习\'");
// \r: 一个回车(回车与换行不同,回车代表光标会移动到当前项)
System.out.println("你好,世界\r哈喽");
}
}

生成说明文档的命令语句

1
javadoc -d d:\\文件地址 -author -version 主类文件名.java

变量

变量的定义

变量是内存中一个存储空间的表示,可以通过操作该变量来对该内存进行操作

  1. 变量的声明
1
数据类型 变量名;
  1. 变量的赋值
1
变量名 = 值;
  1. 变量的声明和赋值的组合
1
数据类型 变量名 = 值;

注意:

  1. 变量必须先声明,后使用
  2. 变量可以在同一类型下不断变化,即可频繁赋值
  3. 变量在同一作用域下,不能同名

+号的使用

  1. 当左右两边都是数值类型时,则做加法运算
  2. 当左右两边有一方为字符串类型,则做拼接运算

数据类型

数据类型的分类

整数类型

Java的整数类型用于存储整数值数据,比如2,3,230等等

细节:

  1. 整型所占的字节空间不会随操作系统的变化而变化,以保证Java程序的可移植性
  2. Java的整型常量默认为int类型,声明long型常量需要在其后加 ‘l’ 或 ‘L’
  3. Java程序中变量常声明为int类型,除非不足以表示大数,才使用long
  4. bit:计算机中最小的存储单位
  5. byte:计算机中基本存储单元,1byte = 8bit

浮点类型

Java的浮点类型可以表示一个小数,比如 123.4,7.8,0.12等等

  1. 浮点数在机器中存放形式为符号位 + 指数位 + 尾数位
  2. 尾数部分可能会丢失,造成精度损失(小数都是近似值)

细节:

  1. 浮点型所占的字节空间不会随操作系统的变化而变化,以保证Java程序的可移植性
  2. Java的浮点型常量默认为double类型,声明float型常量需要在其后加 ‘f’ 或 ‘F’
  3. 浮点型常量有两种表示方式:十进制表示法和科学计数法(例如:5.12e2表示5.12 * 10的2次方,5.12E-2表示5.12 / 10的2次方)
  4. 通常情况下,应该使用double类型,因为它比float类型更精确
  5. 如果使用计算出的浮点数进行比较(例如2.7和8.1/3比较),需采用两数相减的绝对值不超过某个范围来判断两者是否相等,因为计算机计算的原因8.1/3不等于2.7,而等于一个近似2.7的小数

字符类型

字符类型可以表示单个字符,字符类型是char,char是两个字节(可以存放汉字),多个字符可以采用String类进行存储。字符类型可以存储一个数字,当输出该字符类型的变量时,不会打印数字,而是打印该数字在Unicode编码表中对应的字符

细节:

  1. 字符常量是用单引号括起来的单个字符,例如:char c1 = ‘a’
  2. 用双引号括起来的是String类,不能用于字符类型
  3. Java中允许使用转义字符 ‘' 来将其后的字符转变为特殊字符型常量。例如:char c2 = ‘\n’; 其中 ‘\n’ 表示换行符
  4. 在Java中,char的本质是一个整数,在输出时,是unicode编码表对应的字符
  5. 可以直接给char类型的变量赋值一个整数,然后输出时,会按照unicode编码表输出对应的字符
  6. char类型可以进行运算,相当于一个整数,因为它都对应有unicode码
1
2
3
4
5
6
7
8
9
10
11
12
public class CharTest {
public static void main(String[] args) {
char c1 = 'a';
System.out.println(c1); // a
System.out.println((int)c1); // 97
char c2 = 97;
System.out.println(c2); // a
char c3 = 'b' + 1;
System.out.println((int)c3); // 99
System.out.println(c3); // c
}
}

布尔类型

true表示真,false表示假,一般用于选择分支

细节:不可以用0或非0的整数赋值给布尔类型的变量

编码

字符集标准定义了字符与数字码之间的映射关系,编码方案则是规定如何存储该数字码,采用多少个字节来存储该数字码

  1. 字符集标准:
  • Unicode:定义了世界上大部分字符对应的数字码
  1. 编码方案
  • UTF-8:大小可变的方案,字母的数字码使用1个字节存储,汉字的数字码使用3个字节存储
  • UTF-16:固定不变的方案,字母和汉字的数字码都是采用2个字节存储,很浪费空间,但是内存中存储字符都是采用该编码方案存储
  • UTF-32:固定不变的方案,字母和汉字的数字码都是采用4个字节存储
  1. 即使编码方案,又是字符集标准
  • ASCII:定义了128个字符对应的数字码,数字码采用1个字节存储
  • GBK:可以表示汉字,字母的数字码使用1个字节存储,汉字的数字码采用2个字节存储
  • GB2312:可以表示汉字,但是存储的汉字数量少于GBK
  • BIG5:可以表示繁体汉字,台湾、澳门、香港常用

数据类型转换

自动类型转换

当Java程序在进行赋值或者运算时,精度小的类型自动转换为精度大的数据类型,这就是自动类型转换

以下为低精度类型到高精度类型的排序:

细节:

  1. 自动提升原则:表达式结果的类型自动提升为 操作书中最大的类型
  2. boolean类型不参与转换
  3. byte, short 不会和 char 进行相互自动转换
  4. byte,short,char 它们三者之间可以进行计算,在计算时首先转换为int类型(例如:short s1 = 1; short s2 = 2; 则 s1 + s2 的结果是int类型
  5. 将精度大的数据类型变量直接赋值给精度小的数据类型变量,会直接报错
  6. short s1 = 19; 或者 byte b1 = 1; 可以赋值的原因是整数赋值会先判断该常量是否超过该类型的范围,再判断类型。而小数赋值或者变量赋值则是直接判断类型

强制类型转换

自动类型转换的逆过程,将容量大的数据类型转换为容量小的数据类型。使用时要加上强制类型转换符(),但可能造成精度降低或溢出,格外要注意。

1
2
3
4
5
6
7
public class ChangeChar {
public static void main(String[] args) {
double d1 = 1.1;
int i1 = (int)d1;
System.out.println(i1);
}
}

细节:

  1. 强转符号只针对于最近的操作数有效,往往会使用小括号提升优先级
  2. char类型可以保存int的常量值,但不能保存int的变量值,需要强转

基本数据类型和String类型的转换

  1. 基本类型转String类型:将基本类型的值 + “” 即可得到
  2. String类型转基本数据类型:通过基本类型的包装类调用parseXX方法即可得到
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ChangeChar {
public static void main(String[] args) {
// 基本数据类型转字符串类型
boolean b1 = true;
String s1 = b1 + "";
System.out.println(s1);
// 字符串类型转基本数据类型
String s2 = "123";
System.out.println(Integer.parseInt(s2));
// 特别:字符串类型转字符型,表示取出字符串中某一个字符
System.out.println(s2.charAt(0));
}
}

注意:在将String类型转成基本数据类型时,要确保String类型能够转成有效的数据,比如可以把”123”转成整数,但是不能把”hello”转成一个整数

运算符

算术运算符

注意:a % b 的本质是 a - a / b * b 计算后的结果,所以 10 % - 3 的结果为 1,而非 - 1

关系运算符

  1. 关系运算符的结果都是boolean类型,结果要么是true,要么是false
  2. 关系表达式经常用在if结构的条件中或循环结构的条件中
运算符 含义
> 大于
>= 大于或者等于
< 小于
<= 小于或者等于
== 等于
!= 不等于

逻辑运算符

用于连接多个条件(多个关系表达式),最终结果是一个boolean值

  1. a&b:&叫逻辑与,规则:当a和b同时为true,则结果为true,否则为false
  2. a&&b:&&叫短路与,规则:当a和b同时为true,则结果为true,否则为false
  3. a|b:|叫逻辑或,规则:当a和b,有一个为true,则结果为true,否则为false
  4. a||b:||叫短路或,规则:当a和b,有一个为true,则结果为true,否则为false
  5. !a:叫取反,或者非运算。当a为true,则结果为false,当a为false是,结果为true
  6. a^b:叫逻辑异或,当a和b不同时,则结果为true,否则为false

细节:

  1. 逻辑与 & 和短路与 && 的区别:当短路与的一个条件为假时,不会判断第二个条件,直接得到结果为假,而逻辑与无论第一个条件是什么,都会判断第二个条件。故而开发中常用短路与,效率高
  2. 逻辑或 | 和 短路或 || 的区别:短路或的一个条件为真时,不会判断第二个条件,直接得到结果为真,而逻辑或无论第一个条件是什么,都会判断第二个条件。故而开发中常用短路或,效率高

赋值运算符

  1. 基本赋值运算符:=
  2. 复合赋值运算符:+=、-=、*=、/=,%=

细节:

  1. 运算顺序从右向左
  2. 赋值运算符的左边只能是变量,右边可以是常量、表达式、变量
  3. 复合运算符会进行类型转换。例如:byte b = 2; b += 2; 等价于 b = (byte)b + 2; 语句b = b + 2; 是错误的

三元运算符

基本语法:条件表达式 ? 表达式1 : 表达式2

  • 如果条件表达式为true,运算后的结果是表达式1
  • 如果条件表达式为false,运算后的结果是表达式2

细节:表达式1和表达式2要为可以赋给接收变量的类型(或是可以自动转换)

运算符优先级

优先级从上到下依次降低

细节:只有单目运算符(++、–、~、!)和赋值运算符是从右向左运算的

标识符

概念:Java 对各种变量、方法和类邓命名时使用的字符序列称为标识符

命名规则(必须遵守):

  1. 由26个英文字母大小写,0-9,_或$组成
  2. 数字不可以开头
  3. 不可以使用关键字和保留字,但能包含关键字和保留字
  4. Java中严格区分大小写,长度无限制
  5. 标识符不能包含空格。例如 int a b = 90;

命名规范(不是必须):

  1. 包名:多单词组成时所有字母都小写。例如:com.itheima
  2. 类名、接口名:多单词组成时,所有单词的首字母大写。例如:userClass
  3. 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写。例如:tagName
  4. 常量名:所有字母都大写。多单词时每个单词之间用下划线连接。例如:TAX_RATE

键盘输入语句

Java中通过Scanner类扫描用户通过键盘输入的数据,具体使用步骤:

  1. 导入类
  2. 创建该对象的实例
  3. 通过该对象的方法扫描得到具体数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.导入类
import java.util.Scanner;
public class ScannerClass {
public static void main(String[] args) {
// 1.创建Scanner类的实例
Scanner myScanner = new Scanner(System.in);
// 2.使用Scanner类中的方法扫描得到信息
System.out.println("请输入姓名:");
String name = myScanner.next();
System.out.println("请输入年龄:");
int age = myScanner.nextInt();
System.out.println("请输入薪水:");
double salary = myScanner.nextDouble();
System.out.println("此人的信息如下:");
System.out.println("姓名=" + name + " 年龄=" + age + " 薪水=" + salary);
}
}

位运算符

四种进制

二进制:以0b开头。例如:int n1 = 0b1010;

八进制:以0开头。例如:int n2 = 01010;

十进制:常用进制。例如:int n3 = 1010;

十六进制:以0x开头。例如:int n4 = 0x1010;

二进制转换成十进制

规则:从最低位开始,将每个位上的数提取出来,乘以2的(位数-1)次方,然后就和。

例如:0b1011(二进制)= 1 * 2 ^3 + 0 * 2 ^ 2 + 1 * 2 ^ 1 + 1 * 2 ^ 0 = 8 + 0 + 2 + 1 = 11(十进制)

八进制转换成十进制

规则:从最低位开始,将每个位上的数提取出来,乘以8的(位数-1)次方,然后就和。

例如:0234(八进制)= 2 * 8 ^ 2 + 3 * 8 ^ 1 + 4 * 8 ^ 0 = 156(十进制)

十六进制转换成十进制

规则:从最低位开始,将每个位上的数提取出来,乘以16的(位数-1)次方,然后就和。

例如:0x23A(十六进制)= 10 * 16 ^ 0 + 3 * 16 ^ 1 + 2 * 16 ^ 2 = 570(十进制)

十进制转换成二进制

规则:将该数不断除以2,直到商为0为止,然后将每步得到的余数倒过来,就是其对应的二进制

例如:34(十进制)= 0b100010(二进制)

十进制转换为八进制

规则:将该数不断除以8,直到商为0为止,然后将每步得到的余数倒过来,就是其对应的二进制

例如:131(十进制)= 0203(八进制)

十进制转换成十六进制

规则:将该数不断除以16,直到商为0为止,然后将每步得到的余数倒过来,就是其对应的二进制

例如:237(十进制)= 0xED(十六进制)

二进制转换成八进制

规则:从低位开始,将二进制数每三位一组,转成对应的八进制数即可

例如:0b11010 = 0325

二进制转换成十六进制

规则:从低位开始,将二进制数每四位一组,转成对应的十六进制数即可

例如:0b11010101 = 0xD5

八进制转换成二进制

规则:将八进制数的每一位,转成对应的一个三位的二进制数即可

例如:0237 = 0b010011111

十六进制转换成二进制

规则:将十六进制每一位,转成对应的一个四位的二进制数即可

例如:0x23B = 0b001000111011

原码、反码、补码

  1. 二进制数的最高位是符号位:0表示正数,1表示负数
  2. 正数的原码、反码、补码都是一样的(三码合一)、
  3. 负数的反码=它的原码符号位不变,其他位取反
  4. 负数的补码=它的反码+1,负数的反码=负数的补码-1
  5. 0(十进制)的反码和补码都是0
  6. Java没有无符号位,换言之,Java中的数都是有符号的
  7. 在计算机运算的时候,都是以补码的方式来进行运算
  8. 运算结果都是看原码(重点)

按位与/或/异或/取反

按位与&:两位全为1,结果为1,否则位0

按位或|:两位有一个为1,结果为1,否则为0

按位异或^:两位一个为0,一个为1,结果为1,否则为0

按位取反~:每一位都采用0变1,1变0的规则进行运算

计算过程都是用的二进制补码

算术右移/左移、逻辑右移

算术右移>>:低位溢出,符号位不变,并用符号位补溢出的高位

1 >> 2:0000 0000 0000 0001 => 0000 0000 0000 0000。所以结果为0

本质是 1 / 2 / 2 = 0

算术左移<<:符号位不变,低位补0

1 << 2:0000 0000 0000 0001 => 0000 0000 0000 0100。所以结果为4

本质是 1 * 2 * 2 = 4

逻辑右移>>>:低位溢出,高位补0(和算术右移的区别就在于高位是用的0补的)

1 >>> 2:0000 0000 0000 0001 => 0000 0000 0000 0000。所以结果为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
System.out.println(-1 << 2);
System.out.println(" ");
// -1的原码:1000 0000 0000 0001
// -1的反码:1111 1111 1111 1110
// -1的补码:1111 1111 1111 1111
// -1 << 2的补码:1111 1111 1111 1100
// -1 << 2的反码:1111 1111 1111 1011
// -1 << 2的原码:1000 0000 0000 0100 为 -4
// 下面同理
System.out.println(-1 >> 2);
System.out.println(" ");
System.out.println(-1 >>> 2);
}
}

控制结构

顺序

程序从上到下逐行地执行,中间没有任何判断和跳转

故而变量应在使用前声明

分支

单分支

1
2
3
if (条件表达式) {
执行代码块;(可以有多条语句)
}

说明:当条件表达式为ture时,就会执行{}的代码。如果为false,就不执行.

特别说明,如果{}中只有一条语句,则可以不用{}。但是建议写上{}

双分支

1
2
3
4
5
if (条件表达式){
执行代码块1;
} else {
执行代码块2;
}

说明:当条件表达式成立,即执行代码块1,否则执行代码块2。如果执行代码块只有一条语句,则{}可以省略,否则不可省略。

多分支

1
2
3
4
5
6
7
8
9
if (条件表达式1) {
执行代码块1;
} else if (条件表达式2) {
执行代码块2;
}
.......
else {
执行代码块n;
}

说明:

  1. 当条件表达式1成立时,执行代码块1
  2. 只有当条件表达式1不成立,才判断条件表达式2,如果成立,则执行代码块2
  3. 以此类推,如果所有表达式都不成立,则执行else的代码块
  4. 如果没有else,则所有表达式都不成立时,执行后面的代码

嵌套分支

基本介绍:在一个分支结构中又完整的嵌套了另一个完整的分支结构,里面的分支结构称为内层分支,外面的分支结构称为外层分支。建议:不要嵌套超过3层。

1
2
3
4
5
6
7
if (条件表达式1) {
if (条件表达式2) {
// if - else
} else {
// if - else
}
}

switch 分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
switch (表达式) {
case 常量1:
语句块1;
break;
case 常量2:
语句块2;
break;
....
case 常量n:
语句块n;
break;
default:
default语句块;
break;
}

说明:

  1. switch 关键字,表示switch分支
  2. 表达式对应一个值
  3. case 常量1:当表达式的值为常量1,就执行语句块1
  4. break:表达退出switch
  5. 如果和常量1匹配,就执行语句块1,如果没有匹配,就继续匹配常量2
  6. 如果一个都没有匹配上,执行default语句

细节:

  1. 表达式数据类型,应该和case后的常量类型保持一致,或者是可以自动转成可以相互比较的类型,比如输入的是字符,而常量是int
  2. switch(表达式)中表达式的返回值必须是:(byte,short,int,char,enum(枚举),String)
  3. case子句中的值必须是常量,而不能是变量
  4. default子句是可选的,当没有匹配的case时,执行default
  5. break语句用来在执行完一个case分支后使程序跳出switch语句块;如果没有写break,程序会顺序执行到switch结尾,除非遇到break

switch 和 if 推荐使用范围

  1. 如果判断的具体数值不多,而且符合byte、short、int、char,enum[枚举],

String这6种类型。虽然两个语句都可以使用,建议使用swtich语句。

  1. 其他情况:对区间判断,对结果为boolean类型判断,使用if,if的使用范围更广

循环

for 循环控制

1
2
3
for (循环变量初始化; 循环条件; 循环变量迭代) {
循环操作(可以是多条语句);
}
  1. for 关键字,表示循环控制
  2. 循环操作只有一条语句,可以省略{},建议不要省略
  3. 循环条件是返回一个布尔值的表达式
  4. for(;循环判断条件;)中的初始化和变量迭代可以写到其它地方,但是两边的分

号不能省略。

  1. 循环初始值可以有多条初始化语句,但要求类型一样,并且中间用逗号隔开。循环变量迭代也可以有多条变量迭代语句,中间用逗号隔开

增强 for 循环控制

1
2
3
for (变量名 : 数组名或者集合名) {
循环操作(可以是多条语句);
}
  1. 每次从数组或者集合中取出一个元素赋给冒号前的变量
  2. 可以通过操作变量来操作数组或者集合中的每一个元素
  3. 当数组或者集合元素取出完毕后,跳出循环

while 循环控制

1
2
3
4
while (循环条件) {
循环体(语句);
循环变量迭代;
}

do while 循环控制

1
2
3
4
do {
循环体(语句);
循环变量迭代;
} while (循环条件);
  1. do while 是关键字
  2. 和 while 的区别是 它会先执行再判断
  3. 最后一定要有个分号

多重循环控制

  1. 将一个循环放在另一个循环体内,就形成了嵌套循环。其中,for,while,do….while均可以作为外层循环和内层循环(建议两层,最多不超过3层)
  2. 嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为false时,才会完全跳出内层循环,才可以结束外层的当次循环,开始下一次的循环。
  3. 设外层循环次数为m次,内层循环次数为n次,则内层循环体实际上需要执行m*n次。

break

概念:break语句用于终止某个语句块的执行,一般使用在switch或者循环中

1
2
3
4
5
{
....
break;
....
}

细节:

  1. break语句出现再多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块,一般不推荐使用
  2. 如果没有指定标签,默认退出最近的循环体

continue

作用:continue语句用于结束本次循环,继续执行下一次循环

continue语句出现在多层嵌套的循环语句体中时,可以通过标签指明要跳过的是哪一层循环,这个和前面的标签的使用规则一样

1
2
3
4
5
{
.....
continue;
.....
}

return

作用:return使用在方法,表示跳出所在的方法。使用在main方法中,则会退出程序

数组

一维数组

概念:数组可以存放多个同一类型的数据。数组也是一种数据类型,是引用数据类型。

一维数组的使用

  1. 动态初始化
  • 方式一
1
2
3
4
数据类型[] 数组名 = new 数据类型[大小];
或者
数据类型 数组名[] = new 数据类型[大小];
例如:int arr[] = new int[5];

以上代码是创建了一个数组名为arr的数组,它会在内存中开辟5个连续的int空间(4个字节),然后数组名arr则是指向该数组的第一个元素的首地址

  • 方式二
1
2
3
4
5
6
// 1.先声明数组
数据类型[] 数组名;
或者
数据类型 数组名[];
// 2.再创建数组
数组名 = new 数据类型[大小]

以上代码是先声明一个数组,此时该数组是null。创建后,然后数组名则是指向该数组的第一个元素的首地址

  1. 静态初始化
1
数据类型 数组名[] = {元素值, 元素值, .....}

当知道数组有多少元素以及每个元素的具体值,此时可以使用静态初始化

  1. 使用/访问/获取数组的元素:都是采用 数组[下标] 语法

数组的使用细节

  1. 数组是多个相同类型数据的组合,实现对这些数据的统一管理
  2. 数组中的元素可以是任何数据类型,包括基本类型和引用类型,但是不能混用。
  3. 数组创建后,如果没有赋值,有默认值
数据类型 默认值
int、short、byte、long 0
float、double 0.0
char \u0000
boolean false
String null
  1. 使用数组的步骤:
  • 声明数组并开辟空间
  • 给数组各个元素赋值
  • 使用数组
  1. 数组的下标是从0开始的。
  2. 数组下标必须在指定范围内使用,否则报:下标越界异常,比如:int[] arr=new int[5]; 则有效下标为 0-4
  3. 数组属引用类型,数组型教据是对象(obiect)

数组的赋值机制

值拷贝

对于基本数据类型,进行变量赋值操作,会进行值拷贝。本质上是两个变量名在栈中都指向相同的值

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
int n1 = 10;
int n2 = n1;
System.out.println("n1 = " + n1);
System.out.println("n2 = " + n2);
}
}

引用拷贝

对于引用数据类型,变量之间进行赋值操作,则是进行的引用拷贝。本质上是两个变量在栈中都指向相同的地址,该地址为堆中的地址

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = arr1;
arr2[0] = 2;
System.out.println("arr2[0] = " + arr2[0]);
System.out.println("arr1[0] = " + arr1[0]);
}
}

数组拷贝

如果要进行数组的拷贝,而不影响原始数组的数据,可以采用动态创建数组的方法,在堆空间中开辟一块新的空间,再将原始数组的数据赋值给新数组中

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
int[] arr1 = {1, 2, 3};
int[] arr2 = new int[arr1.length];
for (int i = 0; i < arr2.length; i++) {
arr2[i] = arr1[i];
}
}
}

反转数组的算法

方法1:通过交换法实现反转数组的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
int[] arr = {11, 22, 33, 44, 55, 66};
int temp = 0;
int len = arr.length;
// 交换法
for (int i = 0; i < len / 2; i++) {
temp = arr[i];
arr[i] = arr[len - 1 - i];
arr[len - 1 - i] = temp;
}
System.out.println("反转后的数组为");
for (int i = 0; i < len; i++) {
System.out.println(arr[i] + "\t");
}
}
}

方法2:需要新创建一个数组,逆序遍历旧数组,顺序拷贝到新数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
int[] arr1 = {11, 22, 33, 44, 55, 66};
int[] arr2 = new int[arr1.length];
// 逆序遍历旧数组,顺序拷贝到新数组中
for (int i = arr1.length - 1, j = 0; i > - 1; i--, j++) {
arr2[j] = arr1[i];
}
System.out.println("反转后的数组为");
for (int i = 0; i < arr2.length; i++) {
System.out.println(arr2[i] + "\t");
}
}
}

数组扩容

  1. 方法1:新创建一个数组,新数组的容量要大于旧数组,并将旧数组的数据顺序拷贝到就数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
int[] arr = {11, 22, 33, 44, 55, 66};
int[] arrNew = new int[arr.length + 1];
for (int i = 0, j = 0; i < arr.length; i++, j++) {
arrNew[j] = arr[i];
}
arrNew[arrNew.length - 1] = 77;
// 使arr指向新数组
arr = arrNew;
System.out.println("扩容后的数组为");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + "\t");
}
}
}
  1. 方法2:灵活控制是否输入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
int[] arr = {11, 22, 33, 44, 55, 66};
Scanner myScanner = new Scanner(System.in);
do {
System.out.println("请输入你要添加的数据:");
int addNum = myScanner.nextInt();
int[] arrNew = new int[arr.length + 1];
for (int i = 0, j = 0; i < arr.length; i++, j++) {
arrNew[j] = arr[i];
}
arrNew[arrNew.length - 1] = addNum;
// 使arr指向新数组
arr = arrNew;
System.out.println("扩容后的数组为");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + "\t");
}
System.out.println("是否要继续添加数据?y/n");
char flag = myScanner.next().charAt(0);
if (flag == 'n') {
break;
}
} while (true);
System.out.println("已退出添加......");
}
}

二维数组

  1. 声明二维数组的方式和一维数组的声明方式一致
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方式1:动态初始化
数据类型[][] 数组名 = new[2][3];
// 方式2:先声明后创建
数据类型[][] 数组名;
数组名 = new int[2][3];
// 方式3:动态初始化,先动态初始化第一个维度,后面再动态/静态初始化第二个维度
数据类型[][] 数组名 = new[3][];
for (int i = 0; 数组名.length; i++) {
数组名[i] = new int[i + 1];
for (int j = 0; 数组名[i].length; j++) {
数组名[i][j] = i + 1;
}
}
// 方式4:静态初始化
数据类型[][] 数组名 = {{值1, 值2, 值3}, {值4, 值5}, {值6}};
  1. 二维数组的理解:一维数组的每一个元素都是一维数组,则该数组就是二维数组
  2. 获取二维数组元素:数组名[下标1][下标2]
1
2
3
4
5
6
7
8
9
10
11
12
public class Test {
public static void main(String[] args) {
int[][] arr = {{11, 22, 33}, {44, 55, 66}};
// 遍历元素
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + " ");
}
System.out.println("");
}
}
}
  1. 二维数组的内存布局

int[][] arr = new int[2][3]; arr[1][1] = 8;
以上代码中 arr 在内存中的布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class YangHui {
public static void main(String[] args) {
int[][] yangHui = new int[10][];
for (int i = 0; i < yangHui.length; i++) {
yangHui[i] = new int[i + 1];
for (int j = 0; j < yangHui[i].length; j++) {
if (j == 0 || j == yangHui[i].length - 1) {
yangHui[i][j] = 1;
} else {
yangHui[i][j] = yangHui[i - 1][j - 1] + yangHui[i - 1][j];
}
}
}
for (int i = 0; i < yangHui.length; i++) {
for (int j = 0; j < yangHui[i].length; j++) {
System.out.print(yangHui[i][j] + " ");
}
System.out.println(); // 换行
}
}
}

细节:

  1. 二维数组的声明方式有:int[][] 数组名 或者 int[] 数组名[] 或者 int 数组名[][]
  2. 二维数组实际上是由多个一维数组组成,它的各个一维数组的长度可以相同,也可以不相同

面向对象编程

类与对象

  1. 类是抽象的概念,代表一类事物,例如:人类,猫类,即它是数据类型
  2. 对象是具体的,实际的,代表一个具体事物,即是实例
  3. 类是对象的模板,对象是类的一个个体,对应一个具体实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
// 方式1:直接创建对象
Cat cat = new Cat();
// 方式2:先声明后创建
// Cat cat;
// cat = new Cat();
cat.name = "小白";
cat.age = 12;
cat.color = "白色";
}
}

class Cat {
String name = "";
int age = 0;
String color = "";
}

以上代码中对象在JVM中的存在形式如下:

  1. 在Java中创建对象的流程步骤如下:
  • 先加载类的属性和方法信息(在方法区中加载,并且只会加载一次)
  • 在堆中分配空间,进行默认初始化
  • 把地址赋给对象名,即该对象名指向该对象
  • 进行指定初始化,例如 对象名.属性名 = 值;

成员变量(属性)

  1. 概念:成员变量 = 属性 = field(字段)
  2. 属性是类的一个组成部分,一般是基本数据类型,也可以是引用类型(对象、数组)
  3. 通过对象名.属性名访问该对象的特定属性

细节:

  1. 属性的定义语法同变量定义语法一致,示例:访问修饰符 属性类型 属性名;
  2. 属性的定义类型可以是任意类型,包含基本类型和引用类型
  3. 属性如果不赋值,有默认值,规则和数组一致

成员方法

  1. 基本介绍:对于一个类而言,除了属性还有行为,在Java中对于行为,使用方法来描述。例如人类会说话,会睡觉,这就是一种行为
1
2
3
4
5
6
7
class Person {
String name = "";
int age = 18;
public void speak() {
System.out.println(this.name + "说话了"):
}
}
  1. 方法调用机制分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
public static void main(String[] args) {
Person p = new Person();
int resultSum = p.getSum(10, 20);
System.out.println("结果为" + resultSum);
}
}

class Person {
String name = "";
int age = 18;
public int getSum(int num1, int num2) {
int sum = num1 + num2;
return sum;
}
}
  • 当程序执行到方法时,就会开辟一个独立的空间(栈空间)
  • 当方法执行完毕,或者执行到return语句时,就会返回到调用方法的地方,此时该栈空间会被销毁
  • 返回后,继续执行方法后面的代码
  • 当main方法(栈)执行完毕,整个程序退出
  1. 成员方法的好处:
  • 提高代码的复用性
  • 可以将实现的细节封装起来,供其他用户调用
  1. 成员方法的定义
1
2
3
4
public 返回数据类型 方法名 (形参列表) {
方法体;
return 返回值;
}
  • 形参列表:用于接收外部调用成员方法时,传递的参数值
  • 返回数据类型:表示成员方法输出,void表示没有返回值
  • 方法主体:表示为了实现某一功能的代码块
  • return语句不是必须的
  1. 成员方法的使用细节
    1. 返回值使用细节
  • 一个方法最多有一个返回值(要返回多个结果,可以采用集合或者数组)
  • 返回类型可以为任意类型
  • 如果方法要求有返回数据类型,则方法体中最后的执行语句必须是 return 值; 而且要求返回值类型必须和return的值类型一致或兼容
  • 如果方法是void,则方法体中可以没有return语句,或者可以只写return;
  • 方法名应遵循驼峰命名法(第一个单词的首字母小写,后面的单词的首字母必须大写),见名知意
    1. 形式参数使用细节
  • 一个方法可以有0个参数,也可以有多个参数,中间用逗号隔开
  • 参数类型可以为任意类型
  • 调用带参数的方法时,一定对应着参数列表传入相同类型或兼容类型的参数
  • 方法定义时的参数称为形式参数,简称形参;方法调用时的传入参数称为实际参数,简称实参;实参和形参的类型要一致或兼容、个数、顺序必须一致
    1. 方法调用细节
  • 同一个类中方法的调用:直接调用即可
  • 跨类的方法调用:例如A类中的方法调用B类中的方法,需要在A类方法中创建B类的对象,再通过 对象名.B类中的方法名 调用

成员方法传参机制

  1. 对于基本数据类型,成员方法传递的是值(值拷贝),形参内容改变,不会影响实参内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MethodParameter01 {
public static void main(String[] args) {
int a = 10;
int b = 20;
AA obj = new AA();
obj.swap(a, b);
System.out.println("调用swap方法后,a=" + a + " b=" + b);
}
}

class AA {
public void swap(int a, int b) {
System.out.println("交换前,a=" + a + " b=" + b);
int temp = a;
a = b;
b = temp;
System.out.println("交换后,a=" + a + " b=" + b);
}
}
  1. 对于引用数据类型,成员方法传递的是地址(也是值拷贝,不过值是地址),形参会影响实参。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MethodParameter02 {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
AA obj = new AA();
obj.updateArr(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
}
}

class AA {
public void updateArr(int[] arr) {
arr[0] = 100;
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
}

方法的递归调用

本质:方法体中自己调用自己,形成一种递归调用的形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
FactorialClass factorialClass = new FactorialClass();
int result = factorialClass.factorial(5);
System.out.println(result);
}
}

class FactorialClass {
public int factorial(int n) {
if (n == 1) {
return 1;
} else {
return factorial(n - 1) * n;
}
}
}

细节:

  1. 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)
  2. 方法的局部变量是独立的,不会相互影响
  3. 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据
  4. 递归必须向退出递归的条件逼近,否则就会无限递归,出现栈溢出的错误
  5. 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁的准则,同时当方法执行完毕或者返回时,该方法也就执行完毕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class MiGong {
public static void main(String[] args) {
int[][] map = new int[8][7];

// 将迷宫地图第一行和最后一行设置为1,1代表墙
for (int i = 0; i < 7; i++) {
map[0][i] = 1;
map[7][i] = 1;
}

// 将迷宫地图第一列和最后一列设置为1
for (int i = 0; i < 8; i++) {
map[i][0] = 1;
map[i][6] = 1;
}

map[3][1] = 1;
map[3][2] = 1;
map[2][2] = 1; // 测试回溯

// 绘制当前地图
System.out.println("初始地图:");
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}

// 开始帮助小老鼠找路
T t = new T();
t.findWay(map, 1, 1);

// 绘制路线
System.out.println("找到路径后的地图:");
for (int i = 0; i < map.length; i++) {
for (int j = 0; j < map[i].length; j++) {
System.out.print(map[i][j] + " ");
}
System.out.println();
}
}
}

class T {
// map 表示当前地图
// i 表示小老鼠目前所在行
// j 表示小老鼠目前所在列
public boolean findWay(int[][] map, int i, int j) {
// 1. 如果map[6][5]为2,则表示小老鼠已经找到出迷宫的路线
// 2. map[i][j] 为 0 表示可以走但是未测试,1 表示墙,2 表示可以走并且测试过或者正在测试,3 表示该位置上下左右都不可以走
// 3. 找路策略:先下右,后上左
// 4. 返回true,表示该位置走得通,返回false表示该位置走不通
if (map[6][5] == 2) {
// 表示小老鼠已经找到出迷宫的路,整个栈中的数据开始依次出栈
return true;
} else {
if (map[i][j] == 0) {
// 此时测试map[i][j],故而设置为2
map[i][j] = 2;
// 使用策略测试该位置是否可以走
if (findWay(map, i + 1, j)) { // 向下走,走得通,返回true
return true;
} else if (findWay(map, i, j + 1)) { // 向右走,走得通,返回true
return true;
} else if (findWay(map, i - 1, j)) { // 向上走,走得通,返回true
return true;
} else if (findWay(map, i, j - 1)) { // 向左走,走得通,返回true
return true;
} else {
// 如果都走不通,表示该位置上下左右都不可以走,故而设置成 3
map[i][j] = 3;
// 并且返回false,表示该位置走不通,此时会回溯到上一个位置
return false;
}
} else {
// 表示map[i][j] == 1, 2, 3
// 1 表示墙直接返回false
// 2 表示测试过或者正在测试,此时不应该再测试,故而返回false
// 3 表示表示该位置上下左右都不可以走,返回false
return false;
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class HanoiTower {
public static void main(String[] args) {
// 创建对象
Tower tower = new Tower();
// 移动5个圆盘,输出移动步骤
tower.move(5, 'A', 'B', 'C');
}
}

class Tower {
// num 表示要从当前塔移动到目标塔的个数
// curTower 表示当前塔
// tempTower 表示暂时塔
// destTower 表示目标塔
public void move (int num, char curTower, char tempTower, char destTower) {
if (num == 1) {
// 如果移动个数为1,则直接移动到目标塔
System.out.println(curTower + "=>" + destTower);
} else {
// 如果移动个数大于1,则要借助暂时塔移动
// (1)借助目标塔将上面的所有圆盘移动到暂时塔
move(num - 1, curTower, destTower, tempTower);
// (2)将最后一个圆盘直接移动到目标塔
System.out.println(curTower + "=>" + destTower);
// (3)将剩下的圆盘借助当前塔,从暂时塔移动到目标塔
move(num - 1, tempTower, curTower, destTower);
}
}
}

方法重载(overload)

定义:Java中允许同一个类中,多个同名方法的存在,但要求形参列表不一致

作用:

  • 减轻了起名的麻烦
  • 减轻了记名的麻烦
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class OverLoad01 {
public static void main(String[] args) {
MyCalculate myCalculate = new MyCalculate();
System.out.println(myCalculate.Calculate(1, 2));
System.out.println(myCalculate.Calculate(1.1, 2));
}
}

class MyCalculate {
// 方法重载
public int Calculate (int num1, int num2) {
return num1 + num2;
}

public double Calculate (double num1, int num2) {
return num1 + num2;
}
}

细节:

  1. 方法名必须一致
  2. 形参列表:必须不同(形参类型或个数或顺序,至少有一样不同,参数名无要求)
  3. 返回类型:无要求(即返回类型不是构成方法重载的要求)

可变参数

基本介绍:Java允许将同一个类中多个同名同功能但参数个数不同的方法,通过可变参数封装成一个方法

1
2
3
访问修饰符 返回类型 方法名(数据类型... 形参名) {
方法体;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class VarParameter01 {
public static void main(String[] args) {
Methods methods = new Methods();
System.out.println(methods.getSum(1, 3, 100));
}
}

class Methods {
public int getSum (int... nums) {
System.out.println("接收的参数个数为:" + nums.length);
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}

细节:

  1. 可变参数的实参可以为0个或者任意多个
  2. 可变参数的实参可以是数组
  3. 可变参数的本质就是数组,故而使用可变参数时,就采用使用数组的语法
  4. 可变参数可以和普通类型的参数一起放在形参列表,但是必须保证可变参数在最后
  5. 一个形参列表中只能出现一个可变参数

作用域

基本概念:

  • 在Java中,变量可分了局部和全局变量
  • 局部变量是一般指在成员方法中定义的变量
  • 全局变量是指在类的属性,作用域为整个类体,成员方法也能调用该变量
  • 全局变量不赋值可直接使用,因为有默认值,局部变量必须赋值后才能使用,因为没有默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
T t = new T();
t.test();
}
}

class T {
// 全局变量,可不赋值直接使用
int num;

public void test () {
// 局部变量,必须赋值后使用
String str = "hello, world";
System.out.println(num + " " + str);
}
}

细节:

  1. 属性(全局变量)和局部变量可以重名,使用时遵循就近原则
  2. 在同一个作用域中,比如在同一个成员方法中,有两个局部变量,就不能重名
  3. 属性伴随对象的创建而创建,伴随对象的销毁而销毁。局部变量,伴随它的代码块的执行而创建,伴随代码块的结束而销毁,即一次方法的调用
  4. 作用域范围不同
  • 全局变量(属性):可以在本类中使用,或其他类使用(通过对象调用)
  • 局部变量:只能在本类中对应的方法中使用
  1. 修饰符不同
  • 全局变量(属性)可以加修饰符
  • 局部变量不允许加修饰符

构造器

基本介绍:构造方法又叫构造器,是类的一种特殊方法,主要作用是完成对新对象的初始化

特点:

  • 方法名和类名相同
  • 没有返回值
  • 在创建对象时,系统会自动调用该类的构造器完成对对象的初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Constructor01 {
public static void main(String[] args) {
Person p1 = new Person("Tom", 18);
Person p2 = new Person("Rose");
System.out.println("p1的属性:" + p1.name + " " + p1.age);
System.out.println("p2的属性:" + p2.name);
}
}

class Person {
String name;
int age;

public Person(String Pname, int Page) {
name = Pname;
age = Page;
}

public Person(String Pname) {
name = Pname;
}
}

细节:

  1. 一个类可以定义多个不同的构造器,即构造器重载
  2. 构造器名和类名要相同
  3. 构造器没有返回值
  4. 构造器时完成对象的初始化,并不是创建对象
  5. 在创建对象,系统自动调用该类的构造器
  6. 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器
  7. 一旦定义了自己的构造器,默认构造器就被覆盖了,就不能再使用默认无参构造器,除非显示定义一下,即 public 类名() {}

对象创建流程分析

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
Person p = new Person("小倩", 20);
}
}
class Person {
int age = 90;
String name;
Person(String n, int a) {
name = n;
age = a;
}
}

以上代码执行流程分析:

  1. 首先在方法区中加载Person类信息,只加载一次
  2. 再在栈中新建一个独立的空间(用于执行main方法中的代码,称为main空间),执行到Person p = new Person(“小倩”, 20);代码时,在堆中开辟一个空间用于存放对象信息
  3. 初始化对象数据,age初始化为0,name初始化为null,然后显式初始化,将age赋值为90
  4. 再在栈中新建一个独立空间(用于执行有参构造方法),在方法区的常量池中存放字符串小倩,name赋值为存放该字符串数据的地址,age再赋值为20
  5. 此时main空间中有个对象名(对象引用)p赋值为在堆中存放该对象的地址

this 关键字

介绍:Java虚拟机会给每个对象分配 this,代表当前对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Constructor01 {
public static void main(String[] args) {
Person p1 = new Person("Tom", 18);
Person p2 = new Person("Rose");
System.out.println("p1的属性:" + p1.name + " " + p1.age);
System.out.println("p2的属性:" + p2.name);
}
}

class Person {
String name;
int age;

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

public Person(String name) {
this.name = name;
}
}

细节:

  1. this 关键字可以用来访问本类的属性、方法、构造器
  2. this用于区分当前类的属性和局部变量
  3. 访问成员方法的语法:this.方法名(参数列表);
  4. 访问构造器语法:this(参数列表); 注意只能在构造器中使用,一般用于在一个构造器中访问另一个构造器,但是该语法只能写在第一条语句
  5. this不能在类定义的外部使用,只能在类定义的方法中使用

作用

  1. 区分相同名字得类
  2. 当类很多时,可以很好的管理类
  3. 控制访问范围(与访问修饰符有关)

基本语法

1
package com.itcz
  1. package 关键字,表示打包
  2. com.itcz 表示包名

本质

实际上就是创建不同的文件夹来保存类文件

命名规则与规范

  1. 规则:只能包含数字、字母、下划线、小圆点,但不能以数字开头,不能是关键字或保留字
  2. 规范:一般是小写字母+小圆点,例如:com.公司名.项目名.业务模块名

使用细节

  1. 包的导入语法
1
2
3
import 包名.类名;
或者
import 包名.*;

第一条用于引入该包下的特定类

第二条用于引入该包下的所有类

  1. 打包的语法
1
package 包名
  1. 该语法必须放在Java文件的第一行中,并且每个Java文件中都只能包含一条该类型的语句
  2. import导入语法放在打包语法之后,类定义语法直线,可以存在多条导入语句,并且无顺序要求

访问修饰符

访问级别 访问控制修饰符 同类 同包 子类 不同包
公开 public
受保护 protected ×
默认 没有修饰符 × ×
私有 private × × ×

使用注意事项:

  1. 修饰符可以用来修饰类中的属性,成员方法以及类
  2. 只有默认的和public才可以修饰类,并且遵循上述访问权限的特点
  3. 成员方法的访问规则和属性完全一样

封装

  1. 定义:封装就是把抽象出的数据(属性)和对数据的操作(方法)封装在一起,数据被保护在内部,程序的其他部分只有通过被授权的操作(方法),才能对数据进行操作
  2. 作用
  • 隐藏实现的细节(方法),直接调用方法即可
  • 可以在方法中对数据进行验证,保证安全合理
  1. 实现步骤
  • 将属性私有化(不能直接修改属性)
  • 提供公共的set方法,用于对属性判断并赋值
1
2
3
4
public void setXxx(类型 参数名) {
// 加入数据验证的业务逻辑
属性名 = 参数名;
}
  • 提供公共的get方法,用于获取属性的值
1
2
3
public XX getXxx() { // 权限判断
retrun xx;
}
  1. set方法和构造器的组合

当需要使用构造器来给属性赋值时,可以在构造器方法内调用set方法,来起到验证数据的作用

继承

  1. 定义:当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重复定义这些属性和方法,只需要通过extends来声明继承父类即可
  2. 好处:提高代码的复用性
  3. 基本语法
1
2
3
class 子类名 extends 父类 {
// 代码块
}
  1. 子类会自动拥有父类定义的属性和方法
  2. 父类又叫超类、基类
  3. 子类又叫派生类
  1. 细节
  • 子类继承了所有的属性和方法,非私有的属性和方法可以直接访问,但是私有的属性和方法不能在子类中直接访问,要通过公共的方法去访问
  • 子类必须调用父类的构造器,完成父类的初始化,即使不主动调用,也会默认调用父类的无参构造器
  • 当创建子类对象时,不管使用子类的哪个构造器,默认情况下总会去调用父类的无参构造器,如果父类没有提供无参构造器,则必须在子类的构造器中用super去指定使用父类的哪个构造器完成对父类的初始化工作,否则编译不会通过
  • 如果希望指定去调用父类的某个构造器,则显式调用一下
  • super在使用时,需要放在子类构造器的第一行
  • super() 和 this() 都只能放在构造器第一行,因此这两个方法不能共存于一个构造器
  • Java所有类都是Object类的子类
  • 父类构造器的调用不限于直接父类,将一直往上追溯直到Object类(顶级父类)
  • 子类最多只能继承一个父类,即Java中是单继承机制,可以通过A继承B,B继承C 来实现A类继承B类和C类
  • 不能滥用继承,子类与父类之间必须满足子类属于父类的范围的逻辑关系(例如父类是动物,子类是猫)
  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
public class Test {
public static void main(String[] args) {
Son son = new Son();
// 访问顺序:
// 1. 先判断本类中是否有该属性或方法,再判断是否有权访问
// 2. 如果没有,则判断父类是否有该属性或方法,再判断是否有权访问
// 3. 如果父类中也没有,则重复步骤2(访问父类的父类)
// 4. 直到Object类
System.out.println(son.age);
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
// int age = 39;
}
class Father extends GrandPa {
String name = "大头爸爸";
// 如果此时age属性为私有,则System.out.println(son.age);会报错
// 而不会去访问GrandPa中的age属性
// private int age = 39;
int age = 39;
}
class Son extends Father {
String name = "大头儿子";
}

上述代码在Jvm内存的存在形式如下:

加载流程:

  1. 先加载类信息,包括类之间继承的关系
  2. 在堆中开辟空间,基本数据类型数据直接存储在堆中,字符串类型数据存储在方法区的常量池中,该数据对应的地址则存放到堆中
  3. 栈中对象名指向该对象在堆中地址

super 关键字

  1. 基本介绍:super代表父类的引用,用于访问父类的属性、方法、构造器
  2. 基本语法
  • 访问父类的属性,但不能访问父类的私有属性
1
super.属性名;
  • 访问父类的方法,但不能访问父类的私有方法
1
super.方法名(参数列表);
  • 访问父类的构造器
1
super(参数列表);

只能放在构造器的第一句,只能出现一次

  1. 使用细节
  • 当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用super、this、直接访问是一样的结果
  • 当使用super调用方法或属性时,会直接跳过查找子类中的方法或属性,直接去父类进行查找,未找到再查找父类的父类,直到找到Object
  • super的访问不限于直接父类,如果爷爷类和本类中有同名成员,也可以使用super去访问爷爷类的成员;如果多个基类中都有同名成员,使用super访问遵循就近原则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Test {
public static void main(String[] args) {
Son son = new Son();
son.test();
}
}
class GrandPa {
String name = "大头爷爷";
String hobby = "旅游";
}
class Father extends GrandPa {
String name = "大头爸爸";
int age = 39;
}
class Son extends Father {
String name = "大头儿子";
public void test() {
// 父类Father没有hobby属性,则访问GrandPa类中的hobby属性
System.out.println(super.hobby);
}
}

方法重写 override

  1. 定义:方法重写就是子类有一个方法,和父类的某个方法的名称、放回类型、形参列表完全一样,那么这个子类的方法久重写了父类的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
dog.cry();
}
}

class Animal {
public void cry() {
System.out.println("动物叫唤...");
}
}

class Dog extends Animal {
// 重写方法,如果注释掉则会调用Animal中的cry方法
public void cry() {
System.out.println("小狗汪汪叫...");
}
}
  1. 使用细节
  • 子类的方法的形参列表和方法名称,要和父类方法的形参列表和名称完全一样
  • 子类方法的返回类型要和父类方法的返回类型一样,或者是父类返回类型的子类,比如父类为Object,子类方法返回类型是String
  • 子类方法不能缩小父类方法的访问权限(public > protected > 默认 > private)

Object 类详解

equals 方法

  1. 功能:只能判断引用类型,默认判断的是地址是否相等,子类可以重写该方法,用于判断内容是否相等(例如:Interger, String)
  2. 与 == 的区别
  • == 是比较运算符,可以用于比较基本数据类型和引用数据类型
  • == 如果用于基本数据类型,则判断值是否相等
  • == 如果用于引用数据类型,则判断地址是否相等,即是否为同一个对象
  1. Object 中的 equals 方法源码
1
2
3
public boolean equals(Object obj) {
return (this == obj);
}
  1. String 中的 equals 方法源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public boolean equals(Object anObject) {
// 判断是否为原对象
if (this == anObject) {
return true;
}
// 判断运行类型是否为String
if (anObject instanceof String) {
// 向下转型
String anotherString = (String)anObject;
// value 是 String 类中的一个属性,类型为字符数组
int n = value.length;
// 当两者的字符长度相同时,判断每个字符是否一样
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
// 运行类型不是String,则返回false
return false;
}
  1. Interger 中的 equals 方法源码
1
2
3
4
5
6
7
8
9
10
public boolean equals(Object obj) {
// 判断运行类型是否为Integer
if (obj instanceof Integer) {
// value 为 Integer 类中的一个属性,类型为整型
// (Interger)obj 向下转型后调用Integer类中的特有方法intvalue(),得到该对象的内容
// 判断两者内容是否相同,相同返回true,不相同返回false
return value == ((Integer)obj).intValue();
}
return false;
}

hashCode 方法

  1. 功能:通过对象的地址计算出hash值,然后返回hash值。主要是提高具有hash结构的容器的效率
  2. 细节
  • 若两个引用都指向同一个地址,即同一个对象,则hash值是一样的
  • 若两个引用指向不同的地址,则hash值是不一样的
  • hash值是根据地址计算得来,但不能完全将hash值等价于地址

toString 方法

  1. 功能:默认返回 全类名 + @ + hash值的十六进制
1
2
3
4
5
public String toString() {
// (1)getClass().getName() 方法返回对象的类名
// (2)Integer.toHexString(hashCode()) 方法返回对象的hash值的十六进制数
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  1. 细节
  • 子类可以重写Object的toString方法,一般是输出对象的属性值
  • 直接输出对象时,toString方法会被默认调用

finalize 方法

  1. 功能:当对象被回收时,系统自动调用该对象的finalize方法。子类可重写该方法,做一些释放资源的操作
  2. 细节
  • 当某个对象没有任何引用时,则JVM就认为这个对象是一个垃圾对象,就会使用垃圾回收器销毁该对象,在销毁该对象前,会先调用finalize方法
  • 垃圾回收器的调用,是由系统来决定的,也可以通过System.gc()方法主动触发垃圾回收器

多态

  1. 定义:方法或对象具有多种形态。是面向对象的第三大特征,多态是建立再封装和继承基础之上的。
  2. 多态的具体体现
  • 方法的多态:方法的重写和重载就体现多态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
// 传入不同的参数,调用不同的cry方法,体现了多态
dog.cry();
dog.cry("小花");

System.out.println("======");
// 不同的对象,调用的不同的cry方法,体现了多态
Animal animal = new Animal();
animal.cry();
dog.cry();
}
}

class Animal {
public void cry() {
System.out.println("动物叫唤...");
}
}

class Dog extends Animal {
// 重写方法,如果注释掉则会调用Animal中的cry方法
public void cry() {
System.out.println("小狗汪汪叫...");
}
public void cry(String name) {
System.out.println(name + "汪汪叫...");
}
}
  • 对象的多态:对象的编译类型和运行类型可以不一致,编译类型在定义对象时就确定并且无法改变,运行类型可以变化。编译类型看定义时 = 号的左边,运行类型看 = 号的右边
  1. 向上转型
  • 本质:父类的引用指向了子类的对象
  • 语法:父类类型 引用名 = new 子类类型();
  • 特点:

1)可以调用父类的所有成员(需遵循访问权限)

2)不能调用子类特定的方法,因为在编译阶段,调用成员需要通过编译类型决定

3)运行时需看运行类型的具体体现。即调用方法时,按照从子类(运行类型)开始查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test {
public static void main(String[] args) {
// 向上转型
Animal animal = new Dog();
// 先从子类Dog(运行类型)开始查找方法
animal.cry();
// 不可调用,因为调用方法由编译类型决定
// animal.eatBone();
}
}

class Animal {
public void cry () {
System.out.println("动物叫唤...");
}
}

class Dog extends Animal {
// 重写方法,如果注释掉则会调用Animal中的cry方法
public void cry () { // 重写方法
System.out.println("小狗汪汪叫...");
}
public void eatBone () { // 特有方法
System.out.println("小狗吃骨头...");
}
}
  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
public class Test {
public static void main(String[] args) {
// 向上转型
Animal animal = new Dog();
// 先从子类Dog(运行类型)开始查找方法
animal.cry();
// 向下转型
Dog dog = (Dog) animal;
dog.eatBone();
}
}

class Animal {
public void cry () {
System.out.println("动物叫唤...");
}
}

class Dog extends Animal {
// 重写方法,如果注释掉则会调用Animal中的cry方法
public void cry () { // 重写方法
System.out.println("小狗汪汪叫...");
}
public void eatBone () { // 特有方法
System.out.println("小狗吃骨头...");
}
}
  1. 使用细节
  • 属性没有重写一说,属性的调用看的是编译类型
  • instanceof 比较操作符,用于判断对象的运行类型是否为某个类型或某个类型的子类型

动态绑定机制

  1. 当调用对象方法的时候,该方法会和该对象的内存地址(运行类型)绑定
  2. 当调用对象属性时,没有动态绑定机制,哪里声明哪里使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class Test {
public static void main(String[] args) {
A a = new B();
// 此时会先调用B中的sum方法,由于B中没有sum方法,
// 则会调用父类中的sum方法,由于动态绑定,该sum方法中的getI方法
// 会和a的运行类型绑定,则getI方法为B中的,故而输出20
System.out.println(a.sum());
System.out.println(a.sum1());
}
}

class A {
private int i = 20;
public int sum () {
return getI() + 10;
}
public int sum1 () {
return i + 20;
}
public int getI () {
return i;
}
}

class B extends A {
private int i = 10;
// public int sum () {
// return getI() + 10;
// }
// public int sum1 () {
// return i + 20;
// }
public int getI () {
return i;
}
}

多态数组

数组定义类型为父类类型,存储实际元素类型为子类类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Test {
public static void main(String[] args) {
// 定义Person数组,多态数组
Person[] persons = new Person[3];
persons[0] = new Person("tom", 30);
persons[1] = new Student("lisi", 18, 98.9);
persons[2] = new Teacher("zhangsan", 40, 10000);
// 根据运行类型不同,则调用不同的show方法
for (int i = 0; i < persons.length; i++) {
System.out.println(persons[i].show());
}
// instanceOf 判断运行类型后向下转型调用特有方法
for (int i = 0; i < persons.length; i++) {
if (persons[i] instanceof Student) {
System.out.println(((Student)persons[i]).study());
} else if (persons[i] instanceof Teacher) {
System.out.println(((Teacher)persons[i]).teach());
}
}
}
}

class Person {
private String name;
private int age;
public Person (String name, int age) {
this.name = name;
this.age = age;
}
public String show () {
return "name=" + this.name + " age=" + this.age;
}
public String getName () {
return this.name;
}
}

class Student extends Person{
private double score;
public Student (String name, int age, double score) {
super(name, age);
this.score = score;
}

@Override
public String show () {
return super.show() + " score=" + this.score;
}

public String study () {
return super.getName() + "正在学习Java";
}
}

class Teacher extends Person{
private double salary;
public Teacher (String name, int age, double salary) {
super(name, age);
this.salary = salary;
}

@Override
public String show () {
return super.show() + " salary=" + this.salary;
}

public String teach () {
return super.getName() + "正在教授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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class Test {
public static void main(String[] args) {
Worker worker = new Worker("tom", 2000);
Manager manager = new Manager("zhangsan", 6000, 20000);
TestMethod testMethod = new TestMethod();
testMethod.testGetAnnual(worker);
testMethod.testGetAnnual(manager);
testMethod.testWork(worker);
testMethod.testWork(manager);
}
}
class TestMethod {
// 多态参数
public void testGetAnnual (Employee e) {
System.out.println(e.getAnnual());
}

// 多态参数
public void testWork (Employee e) {
if (e instanceof Worker) {
// 向下转型
((Worker) e).work();
} else if (e instanceof Manager) {
// 向下转型
((Manager) e).manage();
}
}
}
class Employee {
private String name;
private double salary;

public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}

public String getName() {
return name;
}

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

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}

public double getAnnual () {
return 12 * this.salary;
}
}
class Worker extends Employee{
public Worker(String name, double salary) {
super(name, salary);
}

public void work () {
System.out.println("工人 is working");
}

@Override
public double getAnnual() {
return super.getAnnual();
}
}
class Manager extends Employee{
private double bonus;
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}

public void manage () {
System.out.println("经理 is managing");
}

public double getBonus() {
return bonus;
}

public void setBonus(double bonus) {
this.bonus = bonus;
}

@Override
public double getAnnual() {
return super.getAnnual() + this.bonus;
}
}

类变量

  1. 定义:类变量又叫做静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
1
2
3
访问修饰符 static 数据类型 变量名;
或者
static 访问修饰符 数据类型 变量名;
1
2
3
类名.类变量名 (说明:类变量是随着类的加载而创建,所以即使没有创建对象实例也可以访问)

对象名.类变量名

静态变量的访问修饰符的访问权限和普通属性一致

  1. 特点:静态变量会被该类的所有对象实例共享
  2. 内存布局:首先栈中的对象引用指向堆中对象的地址,其次堆中的对象的变量名又会指向堆中的静态变量所在的地址,下图就是Child类中静态变量count在JVM内存中的布局(jdk8之后,之前的话静态变量存放在方法区)
  1. 细节
  • 当需要让某个类的所有对象都共享一个变量时,可以考虑使用类变量
  • 用static修饰的变量称为静态变量/类变量,未用static修饰的变量则称为普通变量/普通成员变量/非静态变量
  • 类变量是在类加载时就初始化了,即使未创建对象,只要类加载了就可使用类变量
  • 类变量的生命周期是随类的加载开始,随着类的消亡而销毁

类方法

  1. 定义:类中的方法用static修饰后称为静态方法,又称类方法
1
2
3
访问修饰符 static 数据返回类型 方法名() {}
或者
static 访问修饰符 数据返回类型 方法名() {}
1
2
3
类名.类方法名();
或者
对象名.类方法名();

前提是满足访问修饰符的访问权限

  1. 使用的最佳场景:当不需要创建对象就可以访问访问方法时,就可以使用类方法,例如JDK源码中的Array和Collection等类中就存在大量静态方法,可以直接通过类名就可以使用其中的方阿飞
  2. 细节
  • 类方法和普通方法是都是随着类的加载而加载,将方法的结构信息存储在方法区
  • 类方法可以用类名和对象名调用,普通方法不可通过类名调用
  • 类方法中不可使用和对象有关的关键字,比如this和super。普通方法可以使用
  • 类方法只能直接访问本类的类方法和类变量,而普通方法既可以直接访问本类的非静态成员,也可以直接访问本类的静态成员。但是类方法中可以通过创建对象,用对象名.方法名访问某个类的非静态方法

代码块

  1. 定义:代码块又称初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。但是和方法不同,它没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类或创建对象时隐式调用
1
2
3
[访问修饰符] {
代码体
};

说明:

  • 修饰符可选,只能写static
  • static修饰的代码块称为静态代码块,未修饰的称为普通代码块/非静态代码块
  • 逻辑语句可以是任何语句
  • 最后的分号可选
  1. 细节
  • 静态代码块在类加载时只会执行一次,代码块则是在每次创建对象时都会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SmallChange {
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}

class A {
static {
System.out.println("A 类的静态代码块被执行");
}

{
System.out.println("A 类的代码块被执行");
}
}
  • 类加载的时机:
    • 当创建对象实例时会加载类
    • 创建子类对象实例时,会先加载父类,再加载子类
    • 使用子类的静态成员时,也会先加载父类,再加载子类
  • 使用类的静态成员时,普通代码块不会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SmallChange {
public static void main(String[] args) {
System.out.println(A.n1);
}
}

class A {
public static int n1;
static {
System.out.println("A 类的静态代码块被执行");
}

{
System.out.println("A 类的代码块被执行");
}
}
  • 创建一个对象时,类中初始化代码执行顺序:
    • 先执行静态代码块和静态属性初始化(注意:两者调用优先级一致,若有多个静态代码块和多个静态变量初始化,则按定义顺序执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SmallChange {
public static void main(String[] args) {
A a = new A();
}
}

class A {
static {
System.out.println("A 类的静态代码块被执行");
}
public static int n1 = getN1();
public static int getN1() {
System.out.println("A 类的静态属性初始化被执行");
return 200;
}
}
- 再执行普通代码块和普通属性初始化(注意:两者调用优先级一致,若有多个普通代码块和多个普通属性初始化,则按定义顺序执行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SmallChange {
public static void main(String[] args) {
A a = new A();
}
}

class A {
static {
System.out.println("A 类的静态代码块被执行");
}
private int n2 = getN2();
{
System.out.println("A 类的普通代码块被执行");
}
private static int n1 = getN1();
public static int getN1() {
System.out.println("A 类的静态属性初始化被执行");
return 200;
}
public int getN2() {
System.out.println("A 类的普通属性初始化被执行");
return 100;
}
}
- 最后执行构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SmallChange {
public static void main(String[] args) {
A a = new A();
}
}

class A {
public A() {
System.out.println("A 类的无参构造器被执行");
}
static {
System.out.println("A 类的静态代码块被执行");
}
private int n2 = getN2();
{
System.out.println("A 类的普通代码块被执行");
}
private static int n1 = getN1();
public static int getN1() {
System.out.println("A 类的静态属性初始化被执行");
return 200;
}
public int getN2() {
System.out.println("A 类的普通属性初始化被执行");
return 100;
}
}
  • 构造器的最前面隐藏了super()和调用普通代码块,故而会先调用父类的构造方法,而父类又会先调用父类的父类的构造方法,一直到Object类为止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class SmallChange {
public static void main(String[] args) {
B b = new B();
}
}

class A {
public A() {
// super();
// 调用A的普通代码块
System.out.println("A 类的无参构造器被执行");
}
{
System.out.println("A 类的普通代码块被执行");
}
}

class B extends A{
{
System.out.println("B 类的普通代码块被执行");
}

public B() {
// super();
// 调用B的普通代码块
System.out.println("B 类的无参构造器被执行");
}
}
  • 当一个类继承父类,创建子类对象时,初始化代码执行顺序:
    • 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
    • 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
    • 父类的构造方法
    • 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
    • 子类的构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class SmallChange {
public static void main(String[] args) {
B b = new B();
}
}

class A {
// 静态代码块
static {
System.out.println("A 类的静态代码块被执行"); // (1)
}
// 普通属性初始化
private int n2 = getN2();
// 普通代码块
{
System.out.println("A 类的普通代码块被执行"); // (6)
}
// 静态属性初始化
private static int n1 = getN1();
public static int getN1() {
System.out.println("A 类的静态属性初始化被执行"); // (2)
return 200;
}
public int getN2() {
System.out.println("A 类的普通属性初始化被执行"); // (5)
return 100;
}
// 构造函数
public A() {
// super();
// 调用B的普通代码块
System.out.println("B 类的无参构造器被执行"); // (7)
}
}

class B extends A{
// 静态代码块
static {
System.out.println("B 类的静态代码块被执行"); // (3)
}
// 普通代码块
{
System.out.println("B 类的普通代码块被执行"); // (8)
}
// 普通属性初始化
private int n3 = getN3();
// 静态属性初始化
private static int n4 = getN4();
public static int getN4() {
System.out.println("B 类的静态属性初始化被执行"); // (4)
return 200;
}
public int getN3() {
System.out.println("B 类的普通属性初始化被执行"); // (9)
return 100;
}
// 构造函数
public B() {
// super();
// 调用B的普通代码块
System.out.println("B 类的无参构造器被执行"); // (10)
}
}
  • 静态代码块只能直接调用本类的静态成员,普通代码块可以直接调用本类的任意成员

final 关键字

  1. 功能:被final修饰的类不能被继承,被final修饰的方法不可以被重写,被final修饰的属性/变量不可以被二次修改,但是允许初始化
  2. 细节
  • final修饰的属性又叫做常量,一般用XXX_XXX_XXX来命名
  • final修改的属性在定义时,必须赋初值,并且以后都不能被二次修改,赋值方式有如下3种:
    • 定义时赋值,例如:public final double TAX_RATE = 0.08;
    • 在构造器中赋初始值
    • 在代码块中赋初始值
  • 如果final修饰的属性是静态的,则初始化的位置只能为以下两种:
    • 定义时赋值,例如:public static final double TAX_RATE = 0.08;
    • 在静态代码中赋初始值
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SmallChange {
public static void main(String[] args) {
A a = new A();
}
}

class A {
public static final double TAX_RATE1 = 0.08;
public static final double TAX_RATE2;
static {
TAX_RATE2 = 0.09;
}
}

因为静态属性在类加载时就会被初始化,而构造器则是在创建对象时才会被调用,故而无法在构造器中赋初始值

  • final类不能被继承,但是可以实例化对象
  • 如果类不是final类,但是含有final方法,则该方法虽然不能被重写,但是可以被继承
  • 当一个类被修饰为final类时,它其中的方法没必要用final修饰
  • final不能修饰构造方法
  • final和static往往搭配使用,效率更高。因为不会导致类加载,底层编译器做了优化处理
1
2
3
4
5
6
7
8
9
10
11
12
public class SmallChange {
public static void main(String[] args) {
System.out.println(A.TAX_RATE);
}
}

class A {
public static final double TAX_RATE = 0.08;
static {
System.out.println("静态代码块被执行");
}
}
  • 包装类(Integer、Double、Float、Boolean等)都是final类,String也是final类

抽象类

  1. 定义:当父类中的某些方法需要声明,但是不确定如何实现,可以将其声明为抽象方法,那么这个类就是抽象类
1
2
3
4
5
// 抽象类定义
访问修饰符 abstract class 类名 {
// 抽象方法定义
访问修饰符 abstract 返回类型 方法名(参数列表); // 没有方法体
}
  1. 细节
  • 抽象类是不可以实例化对象的
  • 抽象类中不一定有抽象方法,但是有抽象方法的类一定是抽象类。抽象类中还可以有非抽象方法
  • abstract 只能修饰类和方法,不能修饰属性等
  • 抽象类还是类,可以有类中拥有的所有成员
  • 抽象方法不能有方法体,即不能实现
  • 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为abstract类
  • private、static、final不能与abstract组合修饰方法,因为前三者修饰的方法都不允许被重写,而abstract修饰的方法是需要被重写的

接口

  1. 定义:接口就是将一些没有实现的方法封装到一起,直到某个类要使用的时候,再根据具体情况实现这些方法
1
2
3
4
5
6
7
8
9
10
11
// 接口定义语法
访问修饰符 interface 接口名 {
//属性
//方法(1.抽象方法 2.默认方法 3.静态方法)
}

// 实现接口的类的语法
访问修饰符 class implements 接口名 {
// 自己的成员
// 必须实现的接口的抽象方法
}
  • JDK7之前接口中只有没有方法体的方法
  • JDK8之后允许接口中有静态方法(必须带有static关键字),默认方法(必须带有default关键字),即允许接口中可以有方法的具体实现
  • 接口中的抽象方法可以省略abstract关键字
  1. 细节
  • 接口不能被实例化
  • 接口中所有方法都是public方法,接口中的抽象方法,可不用abstract修饰
  • 一个普通类实现接口,就必须实现该接口的所有抽象方法
  • 抽象类实现接口,可以不用实现接口的抽象方法
  • 一个类同时可以实现多个接口
  • 接口中的属性只能是final的,而且是public static final修饰的。例如:int a = 1; 实际上是 public static final int a = 1;
  • 接口中属性的访问形式:接口名.属性名
  • 一个接口不能继承其他类,但是可以继承多个别的接口
  • 接口的修饰符只能是public和默认
  1. 实现接口和继承类的区别
  • 接口和继承解决的问题不同
    • 继承的价值在于解决代码的复用性和可维护性
    • 接口的价值在于设计好各种规范,让其他类去实现这些方法。即更加灵活
  • 接口比继承更加灵活,继承满足is - a关系,而接口满足like - a关系
  • 接口从一定程度上实现了代码解耦(接口规范性+动态绑定机制)
  1. 接口的多态特性
  • 多态参数:接口引用可以指向实现该接口的类的对象实例
  • 多态数组:接口数组中的每个元素可以指向实现该接口的类的对象实例

以上两种和类的多态特性一致

  • 多态传递:如果一个接口B继承了存在抽象方法的接口A,类C实现了接口B,那么类C必须实现接口A中的抽象方法,即接口之间存在多态传递机制
  1. Java8中的接口新特性:接口中除了可以定义全局常量和抽象方法之外,还可以定义静态方法和默认方法
  • 接口中定义的静态方法只能通过接口名.方法名去调用
  • 只能通过实现接口的类的对象去调用接口的默认方法,如果实现类重写了接口中的默认方法,那么调用时,调用的是重写后的方法
  • 如果实现类继承了父类,而父类和接口中有同名同参数的默认方法,那么实现类(子类)在没有重写此方法的情况下,默认调用的是父类中的方法(类优先原则)
  • 如果实现类实现了多个接口,而多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,会报错(接口冲突),这就必须在实现类中重写此方法
  • 在通过 接口名.super.方法名 在实现类中调用接口中的已经被重写的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* @author 奥数定理
* @version 1.0
*/
public class Test {
public static void main(String[] args) {
// 1.接口中定义的静态方法只能通过接口名.方法名去调用
IA.method1();
// 2.只能通过实现接口的类的对象去调用接口的默认方法,如果实现类重写了接口中的默认方法,那么调用时,调用的是重写后的方法
SubClass subClass = new SubClass();
subClass.method2();
// 3.如果实现类继承了父类,而父类和接口中有同名同参数的默认方法,那么实现类(子类)在没有重写此方法的情况下,默认调用的是父类中的方法(类优先原则)
// 4.如果实现类实现了多个接口,而多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,会报错(接口冲突),这就必须在实现类中重写此方法
subClass.method3();

}
}

class SubClass extends SuperClass implements IA, IB {
@Override
public void method2() {
System.out.println("实现类中的默认方法被调用");
// 5.在通过 接口名.super.方法名 在实现类中调用接口中的已经被重写的方法
IA.super.method2();
IB.super.method2();
}
}

class SuperClass {
public void method3() {
System.out.println("父类中的默认方法被调用");
}
}

interface IA {
public static void method1() {
System.out.println("接口IA中的静态方法被调用");
}
public default void method2() {
System.out.println("接口IA中的默认方法被调用");
}

default void method3() {
System.out.println("接口IA中的默认方法被调用");
}
}

interface IB {
public default void method2() {
System.out.println("接口IB中的默认方法被调用");
}
}

内部类

  1. 定义:一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类,嵌套其他类的类称为外部类(类的五大成员有:属性、方法、构造器、代码块以及内部类)内部类最大特点是可以直接访问私有属性,并且可以体现类与类之间的包含关系
1
2
3
4
5
6
7
8
9
10
11
12
// 外部类
public class 类名 {
// 内部类
calss 类名 {

}
}

// 外部其他类
class 类名 {

}
  1. 内部类的分类
  • 定义在外部类局部位置上(比如方法内)
    • 局部内部类(有类名)
    • 匿名内部类(没有类名)
  • 定义在外部类的成员位置上
    • 成员内部类(没用static修饰)
    • 静态内部类(使用static修饰)

局部内部类

  1. 定义:局部内部类定义在外部类的局部位置,比如方法中,并且有类名
  2. 细节
  • 局部内部类可以直接访问外部类的所有成员,包含私有的属性和方法
  • 不能添加访问修饰符,因为它的地位就是一个局部变量。局部变量是不能使用访问修饰符的,但是可以使用final修饰,因为局部变量可以使用final修饰,这是该类就不能被继承
  • 它的作用域仅仅是指在定义它的方法或者代码块中
  • 可以在定义它的方法或者代码块中创建它的对象,这样就可以访问它其中的属性和方法
  • 外部其他类不能访问局部内部类的成员
  • 如果外部类和局部内部类的成员重名,访问时遵循就近原则,如果要访问外部类的成员,使用 外部类名.this.成员名 进行访问
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
public class LocalInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.f1();
System.out.println("outer的hashCode:" + outer);
}
}

class Outer {
private int n1 = 1;
private void m1() {
System.out.println("Outer m1()方法被调用");
}
// 普通代码块
{
// 局部内部类
class Inner2 {

}
}
public void f1() {
// 局部内部类
class Inner1 {
private int n1 = 2;
public void f2() {
System.out.println("局部内部类的n1=" + n1);
// 可以直接访问外部类的所有成员
// Outer.this指的是外部类的对象,即哪个对象调用了f1方法
// Outer.this就是哪个对象
System.out.println("外部类的n1=" + Outer.this.n1);
System.out.println("Outer.this的hashCode值:" + Outer.this);
m1();
}
}
// f1方法体内部创建Inner对象
Inner1 inner = new Inner1();
inner.f2();
}
}

匿名内部类

  1. 定义:匿名内部类定义在外部类的局部位置,比如方法中,并且没有类名
1
2
3
new 类名或者接口名 {
类体;
}
  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
public class AnonymousInnerClass {
public static void main(String[] args) {
A a = new A();
a.method();
}
}

class A {
private int n1 = 0;
public void method() {
// 需要:只创建一次Tiger对象,调用cry方法,以后都不会创建该类的对象
// 1. 采用传统模式,这时Tiger类会一直存在JVM底层,而且还可以多次创建Tiger的对象
// IA tiger = new Tiger();
// tiger.cry();

// 2.采用匿名内部类,这时类会在底层加载一次后删除该类的信息,以后都不能创建该类的对象
// JVM底层加载匿名内部类的语法:
// class A$1 implements IA{
// @Override
// public void cry() {
// System.out.println("老虎叫唤....");
// }
// }
// JVM底层会在加载完该类后立即创建该类的对象,并将该对象在堆中的地址返回给tiger,最后销毁该类的信息,故而匿名内部类只能使用一次
IA tiger = new IA() {
@Override
public void cry() {
System.out.println("老虎叫唤....");
}
};
tiger.cry();
System.out.println("tiger的运行类型:" + tiger.getClass());
// IA a$1 = new A$1(); // 错误,匿名内部类只能使用一次
}
}

interface IA {
public void cry();
}

// class Tiger implements IA {
// @Override
// public void cry() {
// System.out.println("老虎叫唤....");
// }
// }
  1. 基于类的匿名内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class AnonymousInnerClass {
public static void main(String[] args) {
A a = new A();
a.method();
}
}

class A {
public void method() {
// 需要:只创建一次Son对象,调用speak方法,以后都不会创建该类的对象
// 1. 采用传统模式,这时Son类会一直存在JVM底层,而且还可以多次创建Son的对象
// Father son = new Son("张三");
// son.speak();

// 2.采用匿名内部类,这时类会在底层加载一次后删除该类的信息,以后都不能创建该类的对象
// JVM底层加载匿名内部类语法类似于
/*
class A$1 extends Father{
public A$1(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(super.getName() + "的speak方法被调用");
}
}
*/
// 底层创建son对象的语法类似于:
/*
Father son = new A$1("张三")
*/
// JVM底层会在加载完该类后立即创建该类的对象,并将该对象在堆中的地址返回给son,最后销毁该类的信息,故而匿名内部类只能使用一次
Father son = new Father("张三") {
@Override
public void speak() {
System.out.println(super.getName() + "的speak方法被调用");
}
};
son.speak();
System.out.println("son的运行类型:" + son.getClass());
// Father a$1 = new A$1(); // 错误,匿名内部类只能使用一次
}
}

class Father {
private String name;
public Father(String name) {
this.name = name;
System.out.println("name = " + name);
}

public void speak() {

}

public String getName() {
return this.name;
}
}

// class Son extends Father {
// public Son(String name) {
// super(name);
// }
//
// @Override
// public void speak() {
// System.out.println(super.getName() + "的speak方法被调用");
// }
// }
  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
public class AnonymousInnerClass {
public static void main(String[] args) {
A a = new A();
a.method();
}
}

class A {
public void method() {
// 直接调用匿名内部类的方法
new Father("张三") {
@Override
public void speak(String str) {
System.out.println("匿名内部类的speak方法被调用,接收的信息为" + str);
}
}.speak("hello");
}
}

class Father {
public Father(String name) {
System.out.println("name = " + name);
}

public void speak(String str) {}
}
  • 匿名内部类可以访问外部类的所有成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class AnonymousInnerClass {
public static void main(String[] args) {
A a = new A();
a.method();
}
}

class A {
private int n1 = 0;
public void method() {
// 直接调用匿名内部类的方法
new Father("张三") {
@Override
public void speak(String str) {
// 直接访问外部类的私有变量
System.out.println("n1 = " + n1);
System.out.println("匿名内部类的speak方法被调用,接收的信息为" + str);
}
}.speak("hello");
}
}

class Father {
public Father(String name) {
System.out.println("name = " + name);
}

public void speak(String str) {}
}
  • 作用域仅仅在定义它的方法或代码块中
  • 不能添加访问修饰符,因为它的低位就是要给局部变量
  • 外部其他类不可以访问匿名内部类,因为匿名内部类只是方法或者代码块的局部变量
  • 如果外部类和匿名内部类的成员重名时,匿名内部类访问这些成员的时,默认遵循就近原则,如果想要访问外部类的成员,则可以使用 外部类名.this.成员名 进行访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class AnonymousInnerClass {
public static void main(String[] args) {
A a = new A();
a.method();
System.out.println("a的hashCode值为:" + a);
}
}

class A {
private int n1 = 0;
public void method() {
// 直接调用匿名内部类的方法
new Father("张三") {
private int n1 = 1;
@Override
public void speak(String str) {
// 直接访问匿名内部类的私有变量
System.out.println("匿名内部类的n1 = " + n1);
// 通过 外部类名.this.成员名 访问外部类的私有变量
// A.this指的是外部类的对象,即哪个对象调用了method方法
// A.this就是哪个对象
System.out.println("外部类的n1 = " + A.this.n1);
System.out.println("A.this的hashCode值为:" + A.this);
System.out.println("匿名内部类的speak方法被调用,接收的信息为" + str);
}
}.speak("hello");
}
}

class Father {
public Father(String name) {
System.out.println("name = " + name);
}

public void speak(String str) {}
}
  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
public class AnonymousInnerClass {
public static void main(String[] args) {
// 1.使用匿名内部类
f1(new Animal() {
@Override
public void eat() {
System.out.println("小狗吃骨头");
}
});

// 2.使用传统方法
// Dog dog = new Dog();
// f1(dog);
f1(new Dog());
}

public static void f1(Animal animal) {
animal.eat();
}
}

interface Animal {
void eat();
}

class Dog implements Animal {
@Override
public void eat() {
System.out.println("小狗吃骨头");
}
}

Lambda 表达式

  1. 作用:简化使用匿名内部类的书写

由于匿名内部类的对象只会使用一次,故而可以省略创建对象的复杂写法,强调实现接口时重写方法的逻辑,进而简化匿名内部类的书写语法

  1. 细节
  • Lambda 表达式只能用来简化函数式接口的匿名内部类的写法,函数式接口:有且只有一个抽象方法的接口(不能是抽象类)称为函数式接口,接口上方可以添加 @FunctionalInterface 注解
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
public class AnonymousInnerClass {
public static void main(String[] args) {
// 1.使用匿名内部类
f1(new Animal() {
@Override
public void eat() {
System.out.println("小狗吃骨头");
}
});

// 2.使用传统方法
// Dog dog = new Dog();
// f1(dog);
f1(new Dog());

// 3.使用Lambda表达式
// 方式一:将实现了Animal接口方法的实现类对象赋值给Animal类型对象引用
Animal animal = () -> {
System.out.println("小狗吃骨头");
};
f1(animal);
// 方式二:直接将实现了Animal接口方法的实现类对象作为方法参数进行传递
f1(() -> {
System.out.println("小猫吃鱼");
});
}

public static void f1(Animal animal) {
animal.eat();
}
}

@FunctionalInterface
interface Animal {
void eat();
}

class Dog implements Animal {
@Override
public void eat() {
System.out.println("小狗吃骨头");
}
}
  • Lambda 表达式满足以下条件可以继续省略
    • 参数类型可以直接不写
    • 如果只有一个参数,参数类型可以省略,同时()也可以省略
    • 如果Lambda表达式的方法体只有一行,大括号,分号,return都可以省略不写,但是需要同时省略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.Arrays;
import java.util.Comparator;

public class AnonymousInnerClass01 {
public static void main(String[] args) {
Integer[] arr = {2, 1, 4, -1, 2};
// 1.匿名内部类写法
// Arrays.sort(arr, new Comparator<Integer>() {
//
// @Override
// public int compare(Integer o1, Integer o2) {
// return o1 - o2;
// }
// });

// 2.Lambda表达式完整写法
// Arrays.sort(arr, (Integer o1, Integer o2) -> {
// return o1 - o2;
// }
// );

// 3.Lambda表达式省略写法
Arrays.sort(arr, (o1, o2) -> o1 - o2);

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

成员内部类

  1. 定义:定义在外部类的成员位置上,并且没有static修饰
1
2
3
4
5
6
7
// 外部类
public class 类名 {
// 成员内部类
calss 类名 {

}
}
  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
public class MemberInnerClass {
public static void main(String[] args) {
// 1.方式一
Outer outer = new Outer("lisi");
Outer.Inner inner = outer.new Inner();
inner.show();
// 2.方式二
Outer.Inner innerInstance = outer.getInnerInstance();
innerInstance.show();
}
}
class Outer {
private String name;

public Outer(String name) {
this.name = name;
}

public class Inner {
public void show() {
System.out.println("外部类name=" + name);
}
}

public String getName() {
return name;
}

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

// 返回成员内部类的对象的方法
public Outer.Inner getInnerInstance() {
return new Inner();
}
}
  • 如果成员内部类的成员和外部类的成员重名,在成员内部类中访问该成员,会遵循就近原则,可以使用 外部类名.this.成员名 访问外部类的成员

静态内部类

  1. 定义:定义在外部类的成员位置上,并且没有static修饰
1
2
3
4
5
6
7
// 外部类
public class 类名 {
// 静态内部类
static calss 类名 {

}
}
  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
public class StaticInnerClass {
public static void main(String[] args) {
// 1.方式一
Outer outer = new Outer("lisi");
Outer.Inner inner = new Outer.Inner();
inner.show();
// 2.方式二
Outer.Inner innerInstance1 = outer.getInnerInstance1();
innerInstance1.show();
// 3.方式三
Outer.Inner innerInstance2 = Outer.getInnerInstance2();
innerInstance2.show();
}
}
class Outer {
private static String name;

public Outer(String name) {
Outer.name = name;
}

public static class Inner {
public void show() {
System.out.println("外部类name=" + name);
}
}

public String getName() {
return name;
}

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

// 返回静态内部类的对象的方法
public Outer.Inner getInnerInstance1() {
return new Inner();
}

// 返回静态内部类的对象的静态方法
public static Outer.Inner getInnerInstance2() {
return new Inner();
}
}
  • 如果成员内部类的成员和外部类的静态成员重名,在成员内部类中访问该成员,会遵循就近原则,可以使用 外部类名.静态成员名 访问外部类的静态成员

枚举

  1. 定义:枚举是一种特殊的类,里面包含一组有限的特定的对象
  2. 实现方式
  • 自定义类实现枚举
  • 使用enum 关键字实现枚举

自定义类实现枚举

  1. 实现步骤:
  • 不提供set方法,防止对象属性被修改,因为枚举对象的值通常为只读类型
  • 对枚举对象/属性使用 final + 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class EnumTest01 {
public static void main(String[] args) {
System.out.println(Season.SPRING);
System.out.println(Season.WINTER);
}
}

// 通过自定义类实现枚举
class Season {
private String name;
private String des;

// 内部创建该类的对象,对象名全部大写,并且使用public final static修饰
public final static Season SPRING = new Season("春天", "温暖");
public final static Season SUMMER = new Season("夏天", "炎热");
public final static Season AUTUMN = new Season("秋天", "凉爽");
public final static Season WINTER = new Season("冬天", "寒冷");

// 构造器私有化
private Season(String name, String des) {
this.name = name;
this.des = des;
}

// 只提供get方法
public String getName() {
return name;
}

public String getDes() {
return des;
}

@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", des='" + des + '\'' +
'}';
}
}

使用enum 关键字实现枚举

  1. 实现步骤
  • 使用 enum 关键字 替代 class 关键字
  • 原来的定义常量的语法改为常量名(实参列表)
  • 如果有多个常量(对象),直接使用,号分隔
  • 如果使用enum 关键字来实现枚举,要求将定义常量(对象)的语句写在第一句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class EnumTest02 {
public static void main(String[] args) {
System.out.println(EnumSeason.SPRING);
System.out.println(EnumSeason.WINTER);
}
}

// 通过enum关键字实现枚举
enum EnumSeason {
// 多个常量之间用逗号分隔,并且要写在第一句
SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷");
private String name;
private String des;

EnumSeason(String name, String des) {
this.name = name;
this.des = des;
}

// 只提供get方法
public String getName() {
return name;
}

public String getDes() {
return des;
}

@Override
public String toString() {
return "Season{" +
"name='" + this.getName() + '\'' +
", des='" + this.getDes() + '\'' +
'}';
}
}
  1. 细节
  • 当使用enum 关键字开发枚举类时,默认会继承Enum类,而且该枚举类是一个final类,可以使用javap工具反编译证明,例如上述代码反编译结果如下
+ 传统的定义对象常量的语法简化成了对象常量名(形参列表) + 如果使用无参构造器创建枚举对象,可以将小括号省略 + 当有多个枚举对象时,使用逗号间隔,最后一个用分号结尾 + 枚举对象必须放在枚举类的行首 + 该枚举类的构造器默认是私有的,故而可以省略private修饰符 + 因为enum 关键字开发枚举类时,默认会继承Enum类,故而该枚举类不能再继承其他类(单继承),但是可以实现接口 3. **Enum类的常用方法**:因为enum修饰的枚举类默认会继承Enum类,故而可以调用Enum类的方法 + toString:Enum类已经重写了toString方法了,返回的是当前枚举对象的对象名,子类可重写该方法 + name:返回当前对象名,子类中不能重写 + ordinal:返回当前对象的位置号,默认从0开始 + vlaues:返回当前枚举类中的所有对象常量 + valueOf:将字符串转换为枚举对象,要求字符串必须为已有的常量名,否则报异常 + compareTo:比较两个枚举对象,比较的是编号
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
public class EnumTest02 {
public static void main(String[] args) {
// 调用父类的toString方法
System.out.println(EnumSeason.SPRING);
// 调用父类的name方法
System.out.println(EnumSeason.SPRING.name());
// 调用父类的ordinal方法
System.out.println(EnumSeason.SPRING.ordinal());
// 调用父类的values方法,静态方法,可以通过类名直接调用
EnumSeason[] enumSeasons = EnumSeason.values();
for (EnumSeason i : enumSeasons) {
System.out.println(i);
}
// 调用父类的valueOf方法,静态方法,可以通过类名直接调用
EnumSeason summer = EnumSeason.valueOf("SUMMER");
System.out.println(summer);
// 调用父类的compareTo方法,返回整型
// 返回的是SPRING的编号 - SUMMER的编号的值
System.out.println(EnumSeason.SPRING.compareTo(EnumSeason.SUMMER));
}
}

// 通过enum关键字实现枚举
enum EnumSeason {
// 多个常量之间用逗号分隔,并且要写在第一句
// 编号从0开始,SPRING的编号为0,SUMMER的编号为1,AUTUMN的编号为2,WINTER的编号为3
SPRING("春天", "温暖"), SUMMER("夏天", "炎热"), AUTUMN("秋天", "凉爽"), WINTER("冬天", "寒冷");
private String name;
private String des;

EnumSeason(String name, String des) {
this.name = name;
this.des = des;
}

// 只提供get方法
public String getName() {
return name;
}

public String getDes() {
return des;
}
}

注解

  1. 定义:注解也被称为元数据,用于解释包、类、方法、属性、构造器、局部变量等数据信息。和注释一样,注解不影响程序逻辑,但是注解可以被编译或运行,相当于嵌入在代码中的补充信息
  2. JDK中常用的基本注解
  • @Override:限定于某个方法,用于标记该方法重写了父类的方法
  • @Deprecated:用于表示某个程序元素(类、方法等)已过时
  • @SuppressWarning{“”}:抑制编译器警告,在双引号中添加相应的字符串,可以抑制对应的警告。它起作用的地方和它放置的位置有关,在某个语句上就抑制该语句的警告,在方法和类等位置同理

@interface 并不是表示接口,而是表示注解

@Target 是元注解,它是修饰注解的注解

1
2
3
4
5
// @Target(ElementType.METHOD) 表示该注解只能用于方法中
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  1. 自定义注解
  • 基本语法:
1
2
3
public @interface 注解名 {
String value default "hello";
}
  • 内部定义成员,通常使用value进行接收
  • 可以指定成员的默认值,使用default定义
  • 如果自定义注解没有成员,表明此注解为一个标识作用
  • 如果注解有成员,那么使用注解时,必须指明成员的值
  1. Java8中注解的新特性
  • 可重复性:在自定义注解上声明@Repeatable元注解,并且声明成员值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Repeatable(MyAnnotations.class)
public @interface MyAnnotation {
String value;
}

public @interface MyAnnotations {
MyAnnotation[] value;
}

public class Test {
// 可重复性
@MyAnnotation(value = "hello")
@MyAnonotation(value = "hi")
public void method1() {}
}
  • 类型注解:Java8之后,关于元注解@Target的参数类型ElementType枚举值多了两个:TYPE_PARAMETER,TYPE_USE
    • TYPE_PARAMETER:表示该注解能写在类型变量的声明语句中
    • TYPE_USE:表示该注解能写在使用类型的任何语句中

JDK中常见的元注解

定义:用来修饰注解的注解

Retention

  1. 功能:只能用来修饰一个注解的定义,用于指定该注解可以保留多长时间,@Retention包含一个RetentionPolicy类型的成员变量,使用@Retention时必须为该成员变量指定值
1
2
@Retention(RetentionPolicy.SOURCE)
// 注解定义
  1. 可以指定的值有三种
  • RetentionPolicy.SOURCE:编译器使用后,直接丢弃这种策略的注释。通俗来讲,就是该注解只在源码生效,例如@Override注解,就只是在使用javac工具编译源码(源文件)时判断源码内子类是否重写方法
  • RetentionPolicy.CLASS:这是默认值,编译器将把注解记录在class 文件中。当运行Java 程序时,JVM不会保留注解。通俗来讲,使用java工具来加载class文件和使用javac工具编译源码(源文件)时该注解生效
  • RetentionPolicy.RUNTIME:编译器将把注解记录在class 文件中.当运行Java 程序时,JVM会保留注解。程序可以通过反射获取该注释。通俗来讲,用java工具来加载class文件、使用javac工具编译源码(源文件)和运行Java程序时该注解生效

Target

功能:用来修饰注解定义,用于指定被修饰的直接能用于修饰那些程序元素。@Target也包含一个名为value的成员变量,类型为字符串数组,故而可以接收多个

1
@Target(value={ElementType.METHOD})

Document

功能:用于指定被该元注解修饰的注解将被javadoc工具提取成文档,即在生成文档时,可以看到该注解

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

Inherited

功能:被它修饰的注解将具有继承性,如果某个类使用了被该元注解修饰的注解,则其子类将自动具有该注解

异常

  1. 定义:在Java程序运行过程中出现的不正常情况称为异常(开发过程中的语法错误和逻辑错误不是异常)
  2. 分类
  • Error:Java虚拟机无法解决的严重问题,Error是严重错误,系统会崩溃。如:JVM系统内部异常,资源耗尽异常等等。
  • Exception:其他因编程错误或偶然的外在因素导致的一般性问题,可以使用针对性的代码进行处理。如:空指针访问,试图读取不存在的文件,网络连接中断等等。Exception又分为两大类:运行时异常(程序运行过程中出现的异常)和编译时异常(编译时,编译器检查出的异常)

异常体系图

常见的运行时异常

  1. NullPointerException:空指针异常,当程序试图在需要对象的地方使用 null 时,抛出该异常
  2. ArithmeticException:数学运算异常,当出现异常的运算条件时,抛出该异常
  3. ArrayIndexOutOfBoundsException:数组下标越界异常,用非法索引访问数组时抛出的异常,当索引为负数或者大于等于数组大小,则该索引为非法索引
  4. ClassCastException:类型转换异常,当试图将对象强制转换为不是该实例对象的子类时,会抛出该异常
  5. NumberFormatException:数字格式不正确异常,当程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。使用该异常可以确保输入的是满足条件的数字

异常处理方式

定义:当异常发生时,对异常的处理

try-catch-finally和throws二选一

try-catch-finally 处理机制

  1. 功能:用于处理可能出现异常的代码,防止程序中断
1
2
3
4
5
6
7
8
9
10
try {
// 可能出现异常的代码
} catch (Exception e) {
// 当异常发生时,系统将异常封装成Exception的对象e,传递给
// catch区域,得到异常对象后对该对象进行处理
// 注意:没有发生异常,将不会执行catch代码块的逻辑
} finally {
// 不管代码是否有异常发生,始终会执行该区域代码
// 通常将释放资源的代码放在该区域
}
  1. 细节
  • 如果异常发生了,则try代码块中出现异常的代码后面的代码都不会运行,直接进入catch代码块
  • 如果未发生异常,则顺序执行try代码块中的代码,不会执行catch代码块中的代码
  • 无论是否发生异常,finally代码块的代码都会执行
  • 如果有多个异常,可以使用多个catch块分别处理各个异常,但是要求处理子类异常catch代码块必须写在父类异常之前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {
public static void main(String[] args) {
try {
int n1 = 1 / 0;
String str = null;
System.out.println(str.length());
} catch (NullPointerException e) {
System.out.println("空指针异常:" + e.getMessage());
} catch (ArithmeticException e) {
System.out.println("算法运算异常:" + e.getMessage());
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
// 资源释放代码
}
}
}
  • 可以直接使用try-finally,但是这种用法相当于没有捕获异常,因此出现异常,程序会直接崩溃

throws 处理机制

  1. 功能:当方法中可能出异常的代码没有用try-catch-finally处理时,该方法会通过throws抛出该异常给调用该方法的地方,依次抛出,直到抛出到JVM。JVM处理异常的方式:直接输出异常信息后退出程序
1
2
3
public class Test {
public void f1() throws 异常列表 {}
}
  • 如果没有显式处理异常,则默认采用throws处理机制
  • 异常列表中一般是方法可能抛出的异常类或者它的父类
  1. 细节
  • 对于编译时异常,必须通过try-catch或者throws进行处理
  • 对于运行时异常,如果不进行处理,默认使用throws进行处理
  • 子类重写父类时,对抛出异常的规定:子类重写的方法,所抛出的异常类型要么和父类抛出的异常一致,要么为父类抛出的异常类型的子类型
1
2
3
4
5
6
class Father {
public void method() throws RuntimeException{}
}
class Son extends Father {
public void method() throws NullPointerException{}
}
  • try-catch和throws两种处理机制二选一即可

自定义异常

步骤

  • 自定义异常类名,并继承Exception或者RuntimeException
  • 如果是继承了Exception,属于编译时异常
  • 如果是继承了RuntimeException,属于运行时异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
int age = 180;
if (!(age >= 18 && age <= 120)) {
throw new AgeException("输入的年龄应处于18到120之间");
}
System.out.println("你输入的年龄范围正确");
}
}
class AgeException extends RuntimeException {
public AgeException(String message) {
super(message);
}
}

throw 和 throws的区别

**** 意义 位置 后面跟的语法
throws 异常处理的一种方式 方法声明处 异常类型
throw 手动生成异常对象的关键字 方法体中 异常对象

常用类

包装类

定义:针对八种基本数据类型设计的引用类型

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

上图中后六种包装类都继承了Number类

拆箱与装箱

定义:将基本数据类型转换成包装类型就称之为装箱,将包装类转换为基本数据类型称之为拆箱

JDK5之前都是手动装箱和手动拆箱,JDK5后都是自动装箱和自动拆箱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
// 手动装箱
int n1 = 100;
Integer integer = new Integer(n1);
Integer integer1 = Integer.valueOf(n1);
// 手动拆箱
int n2 = integer.intValue();
// 自动装箱
Integer integer2 = n2; // 底层还是用的 Integer.valueOf(n2);
// 自动拆箱
int n3 = integer2; // 底层用的还是integer2.intValue();
}
}

其他包装类的拆箱装箱代码同理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Wrapper03 {
public static void main(String[] args) {
Integer integer1 = new Integer(1);
Integer integer2 = new Integer(1);
System.out.println(integer1 == integer2); // false

// 自动装箱底层是调暗勇Integer.valueOf方法
/*
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
*/
// 源码解析:当实参处于-128到127时,返回IntegerCache类中的属性cache数组中的一个元素,否则创建一个新的Integer对象
Integer integer3 = 1;
Integer integer4 = 1;
System.out.println(integer3 == integer4); // true

Integer integer5 = 128;
Integer integer6 = 128;
System.out.println(integer5 == integer6); // false
}
}

包装类与String类的相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Wrapper02 {
public static void main(String[] args) {
// 包装类转换为字符串类型
Integer n = 100;
// 方式一
String str = n + "";
// 方式二
String str1 = String.valueOf(n);
// 方式三
String str2 = n.toString();

// 字符串类型转换为包装类
String str3 = "123";
// 方式一
Integer integer = new Integer(str3);
// 方式二
Integer integer1 = Integer.parseInt(str3);

System.out.println("ok~~~");
}
}

其他包装类同String类的转换同理

String 类

  1. 定义:String 对象用于保存字符串,也就是一组字符序列,字符串常量对象是用双引号括起来的字符序列。字符串的字符使用Unicode字符编码,一个字符(无论是字母还是汉字)占两个字节
1
2
3
4
5
String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[] a);
// 从一个字符数组的某个位置开始,截取多少个字符转换为字符串类型
String s4 = new String(char[] a, int startIndex, int count);
  1. 字符串常量赋值和new 创建String对象的区别
1
2
3
4
// 方式一
String s1 = "cz";
// 方式二
String s2 = new String ("cz");
  • 方式一:先从常量池查看是否有”cz”存在,如果有,则直接指向,如果没有则重新创建,然后指向。s1最终指向的是常量池中的空间地址
  • 方式二:先在堆中创建空间,里面维护了value属性,如果常量池中没有”cz”,则重新创建,如果有,则指向常量池的”cz”的地址。
  1. String 的继承类和实现接口

串行化表示该类型可以再网络上进行传输

  1. 细节
  • String 类是一个final类,不能被其他类继承
  • String 类有属性 private final char value[]; 用于存放字符串内容,该属性是一个final属性,故而它的**地址**是不可修改的
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
final char[] value = {'t','o','m'};
value[2] = 'h';
char[] v2 = {'t','o','h'};
// value = v2;
}
}
  • 字符串常量 + 字符串常量 赋值给一个字符串变量 和 字符串变量 + 字符串变量 赋值给一个字符串变量的区别:
    • 第一种赋值方式的字符串变量会直接指向两个常量的组合在常量池中的地址
    • 第二种赋值方式的字符串变量会指向新创建的字符串对象在堆中的地址,而该对象中的value数组则会指向两个字符串变量的value数组指向的字符串常量的组合在常量池中的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
public static void main(String[] args) {
String a = "123";
String b = "456";
/*
底层执行的代码类似于:
1. StringBuilder sb = new StringBuilder();
2. Sb.append("123");
3. sb.append("456");
4. String c = sb.toString();
*/
String c = a + b;
/*
该代码会被编译器优化为:
String d = "123456";
*/
String d = "123" + "456";
System.out.println(c == d);
}
}

总结:字符串常量相加,则该变量指向常量池。字符串变量相加,则该变量指向字符串对象在堆中的地址

  1. String 常用方法
  • equals:区分大小写,判断内容是否相等
  • equalsIgnoreCase:忽略大小写,判断内容是否相等
  • length:获取字符的个数,字符串的长度
  • indexOf:获取字符在字符串中第一次出现的索引,索引从0开始,如果找不到,则返回-1
  • lastIndexOf:获取字符在字符串中最后一次出现的索引,索引从0开始,如果找不到,则返回-1
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
String a = "we@we@";
// 获取@在字符串中第一次出现的索引
System.out.println(a.indexOf('@'));
// 获取we在字符串中第一次出现的索引,该索引为w所在的索引
System.out.println(a.indexOf("we"));
// 获取@在字符串中最后一次出现的索引
System.out.println(a.lastIndexOf('@'));
// 获取we在字符串中最后一次出现的索引,该索引为w所在的索引
System.out.println(a.lastIndexOf("we"));
}
}
  • substring:截取指定范围的字串
1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
String a = "hello,world";
// 从第六个索引开始,截取到最后,索引从0开始
System.out.println(a.substring(6));
// 从第0个索引开始,截取到第五个索引,但是不包括第五个索引,索引从0开始
System.out.println(a.substring(0, 5));
// 从第2个索引开始,截取到第五个索引,但是不包括第五个索引,索引从0开始
System.out.println(a.substring(2, 5));
}
}
  • trim:取出前后空格
  • charAt:获取某索引处的字符,注意不能使用 字符串名[下标] 的方式去获取
  • matches(String regStr):根据正则表达式进行匹配,返回boolean类型
  • replaceAll(String regStr, String repalcement):根据正则表达式进行匹配,然后使用replacement的内容替换匹配到的所有子字符串
  • toUpperCase:将字符串全部转为大写字母
  • toLowerCase:将字符串全部转为小写字母
  • concat:拼接字符串,返回字符串
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
String str = "hello";
String newStr = str.concat(",world").concat(",你好").concat(",世界");
System.out.println(str);
System.out.println(newStr);
}
}
  • repalce:替换字符串中的某些字符串,返回字符串
1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
String str = "ab,b,ab,b";
String newStr = str.replace("ab", "b");
System.out.println(str);
System.out.println(newStr);
}
}
  • split:根据字符串分隔字符串,返回字符串数组,可能需要用到转义字符,支持根据正则表达式进行分隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test {
public static void main(String[] args) {
String a = "hello,world,你好,世界";
String[] splits = a.split(",");
for (String split : splits) {
System.out.println(split);
}
String str = "E:\\temp\\input.txt";
// 使用转义字符
splits = str.split("\\\\");
for (String split : splits) {
System.out.println(split);
}
}
}
  • compareTo:依次对比两个字符串的每个字符,如果有字符不同,则返回第一个字符串中的字符 - 第二个字符串中的字符的值,如果前面字符都相同,则返回第一个字符串的长度 - 第二个字符串的长度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test {
public static void main(String[] args) {
String str1 = "jack";
String str2 = "jackjack";
/*
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;

int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
*/
// 由于前面四个字符相同,故而返回str1.length() - str2.length()
System.out.println(str1.compareTo(str2));
}
}
  • toCharArray:将字符串转换为字符数组
  • format:格式化字符串,%s 字符串,%c 字符,%d 整型,%.2f 浮点型
1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
String name = "张三";
String gender = "男";
int age = 18;
String str = String.format("姓名:%s,性别:%s,年龄:%d", name, gender, age);
System.out.println(str);
}
}

StringBuffer 类

  1. 定义:StringBuffer 代表可变的字符序列,可以对字符串内容进行增删。很多方法和String相同,但是StringBuffer是可变长度的。
  2. StringBuffer 和 String 的区别
  • String 的底层是 private final char[] value 属性去指向常量池中的字符串常量(字符数组),当要修改该String类型的对象的字符串内容时,会在常量池中创建一个字符串常量,然后value属性去指向该字符串常量。这就导致了String对象每次更新内容都要修改value指向的地址,进而效率低下
  • StringBuffer 的底层是 private char[] value 属性指向堆中的字符串变量(字符数组),当要修改该StringBuffer类型的对象的字符串内容时,可以直接操作该字符数组,不用更换地址。只有当该字符数组的大小(默认是16)不够用时,才会进行扩容处理,此时才会更新地址,进而效率比较高
  1. StringBuffer 常用构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test {
public static void main(String[] args) {
// 1. 默认构造器,会创建一个16大小的char数组类型的value属性
/*
public StringBuffer() {
super(16);
}
*/
StringBuffer sb1 = new StringBuffer();
// 2. 通过构造器,创建指定大小的value属性
/*
public StringBuffer(int capacity) {
super(capacity);
}
*/
StringBuffer sb2 = new StringBuffer(100);
// 3. 通过给一个String类型的对象创建一个该字符串的大小 + 16 大小的value属性
/*
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
*/
StringBuffer sb3 = new StringBuffer("String");
}
}
  1. String 和 StringBuffer 的相互转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
public static void main(String[] args) {
// String -> StringBuffer
String str = "hello String";
// 方式一:使用构造器
// 注意:返回的才是StringBuffer对象,对str本身无影响
StringBuffer stringBuffer1 = new StringBuffer(str);
// 方式二:使用append方法
StringBuffer stringBuffer2 = new StringBuffer();
stringBuffer2 = stringBuffer2.append(str);

// StringBuffer -> String
StringBuffer stringBuffer3 = new StringBuffer("com.itcz");
// 方式1:使用StringBuffer提供的 toString 方法
String s = stringBuffer3.toString();
// 方式2:使用构造器来完成
String s1 = new String(stringBuffer3);
}
}
  1. StringBuffer 的常用方法
  • append:添加字符串
  • delete(start, end):删除字符串中索引从start开始,end结束(不包括end)的字符串
  • replace(start, end, String):将字符串中索引从start开始,end结束(不包括end)的字符串替换为第三个参数的内容
  • indexOf:查找子串在字符串第一次出现的索引,如果找不到返回-1
  • insert(index, String):在字符串的索引为index的位置插入第二个参数的内容,原来索引为index字符后面的所有字符(包括index索引所在的字符)全部自动后移
  • length:获取字符串的长度
  1. 细节
  • StringBuffer 的直接父类是 AbstractStringBuilder,该类中有属性char[] value,不是final类型,故而存储在堆中,并且value属性是用来存放字符串内容的
  • StringBuffer 是一个 final 类,不能被继承,它还实现了 Serializable 接口,即StringBuffer的对象可以串行化,即可以保存在文件中,还可以在网络中传输

StringBuilder 类

  1. 定义:一个可变的字符序列。此类提供一个与StringBuffer兼容的API,但不保证同步(StringBuilder 不是线程安全)。该类被设计用作StringBuffer的一个简易替换,用在字符串缓冲区被单个线程使用的时候。如果可能,建议优先采用该类,因为在大多数实现中,它比StringBuffer要快。
  2. String 、StringBuffer 和 StringBuilder 的区别
  • StringBuffer 和 StringBuilder 非常类似,均代表可变的字符序列,而且方法也是一样
  • String 则是不可变字符序列,效率低,但是复用率高
  • StringBuffer 是可变字符序列,效率高(增删)、线程安全
  • StringBulider 是可变字符序列,效率最高,但是线程不安全

String类型的对象进行大量 += 字符串的操作,会导致大量副本字符串对象留在内存中,降低效率。总结:如果要对字符串进行大量修改,不要使用String类型

  1. 细节
  • StringBuilder 的直接父类是 AbstractStringBuilder,并且实现了Serializable接口,故该类的对象可以串行化
  • StringBuilder 是final类,不能被继承,并且该对象字符序列仍然是存放在其父类的value属性中,因此字符序列存放在堆中
  • StringBuilder 的所有方法都没有做同步互斥处理,因此推荐在单线程的情况下使用StringBuilder

Math 工具类

  1. 定义:专用于操作数字的final类,提供了大量的静态方法
  2. 常用方法
  • abs:求参数的绝对值
  • pow:求第一个参数的第二个参数的次方
  • ceil:向上取整,返回double类型
  • floor:向下取整,返回double类型
  • round:四舍五入,返回double类型
  • sqrt:求开方
  • random:返回一个大于等于0,小于1的随机小数

获取一个a -b 之间(包括a和b)的一个随机整数的公式:

Math.floor(Math.random() * (b - a + 1) )

  • max:求两个数的最大值
  • min:求两个数的最小值

Arrays 工具类

  1. 定义:该类是专用于操作和管理数组的final类,内部定义了一系列静态方法
  2. 常用方法
  • toString(数组类型的对象):返回数组的字符串形式
  • sort:对数组进行排序

该方法可分为两种,默认排序和定制排序。

  • sort(数组类型的对象):默认从小到大对数组进行排序
  • sort(数组类型的对象,实现Comparator接口的对象):通过实现Comparator接口中的compare方法,影响数组排序的排序规则(是从小到大还是从大到小),其底层排序算法用的二分排序树
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.Arrays;
import java.util.Comparator;

public class Test {
public static void main(String[] args) {
Book[] books = new Book[4];
books[0] = new Book("红楼梦", 100);
books[1] = new Book("金瓶梅", 90);
books[2] = new Book("青年文摘", 5);
books[3] = new Book("Java从入门到放弃", 300);
bubble(books, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
Book b1 = (Book) o1;
Book b2 = (Book) o2;
// 相当于 arr[j].getPrice() > arr[j+1].getPrice(),所以排序规则为从小到大
if (b1.getPrice() - b2.getPrice() > 0) {
return 1;
} else if (b1.getPrice() - b2.getPrice() == 0) {
return 0;
} else {
return -1;
}
}
});
System.out.println(Arrays.toString(books));
}

// 定制冒泡排序方法
public static void bubble(Object[] arr, Comparator<Object> comparable) {
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (comparable.compare(arr[j], arr[j + 1]) > 0) {
Object temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}

class Book {
private String name;
private double price;

public Book(String name, double price) {
this.name = name;
this.price = price;
}

public String getName() {
return name;
}

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

public double getPrice() {
return price;
}

public void setPrice(double price) {
this.price = price;
}

@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
  • binarySearch(数组类型的对象, 值):要求数组必须是升序的,通过二分查找法查找该值在数组的下标
  • copyOf(数组类型的对象, 拷贝的个数):拷贝数组,返回一个新数组
  • fill(数组类型的对象, 元素值):将数组中所有元素都改为第二个参数值
  • equals(数组类型的对象,数组类型的对象):对比两个数组的每个位置元素是否一样,不一样返回false,反之返回true
  • asList(数组类型的对象):会将数组中的所有数据转成一个List集合

System 工具类

  1. 定义:和系统指令相关的final类,内部有一系列的静态方法
  2. 常用方法
  • exit:退出当前程序
  • arraycopy(src, srcPos, dest, destPos, length):复制数组元素,比较适合底层调用,Arrays.copyOf方法底层就是调用的System.arraycopy方法。参数说明:
    • src:原数组
    • srcPos:从原数组的哪个位置开始拷贝
    • dest:目标数组
    • destPos:拷贝的元素从目标的数组的哪个索引开始赋值
    • length:拷贝的数据个数
  • currentTimeMillens:返回当前时间距离1970-1-1的毫秒数
  • gc:运行垃圾回收器

BigInteger 类

  1. 定义:用来处理和存储非常大的整数的final类
1
2
3
4
5
6
7
8
9
10
import java.math.BigInteger;

public class BigInteger01 {
public static void main(String[] args) {
BigInteger bigInteger1 = new BigInteger("123456789123456789");
BigInteger bigInteger2 = new BigInteger("10");
BigInteger bigInteger = bigInteger1.add(bigInteger2);
System.out.println(bigInteger);
}
}
  1. 常用方法
  • add:加法
  • substrct:减法
  • multiply:乘法
  • divide:除法

BigDecimal 类

  1. 定义:用来处理和存储非常大的小数的final类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.math.BigDecimal;
import java.math.RoundingMode;

public class BigDecimal01 {
public static void main(String[] args) {
BigDecimal bigDecimal1 = new BigDecimal("123.123123456456");
BigDecimal bigDecimal2 = new BigDecimal("1.2");
System.out.println(bigDecimal1.add(bigDecimal2));
System.out.println(bigDecimal1.subtract(bigDecimal2));
System.out.println(bigDecimal1.multiply(bigDecimal2));
// 不加RoundingMode.CEILING,结果可能为无限不循环小数,所以可能报异常
// 加了后结果会保留到 分子 精度
System.out.println(bigDecimal1.divide(bigDecimal2, RoundingMode.CEILING));
}
}
  1. 常用方法
  • add:加法
  • substrct:减法
  • multiply:乘法
  • divide:除法

日期类

Date 类

  1. 定义:第一代日期类,可以用来表示时间,精确到毫秒
1
2
3
4
// 1.获取当前系统时间
Date date1 = new Date();
// 2.与1970-1-1相差指定毫秒的时间
Date date2 = new Date(9867);
  1. SimpleDateFormat 类:格式和解析日期的类
  • format:格式化时间输出形式,返回字符串类型
  • parse:解析时间,得到Date对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {
public static void main(String[] args) throws ParseException {
// 1.获取当前系统时间
Date date1 = new Date();
// 2.与1970-1-1相差指定毫秒的时间
Date date2 = new Date(9867);
System.out.println("date2 = " + date2);

SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss E");
// 格式化日期输出格式
String format = sdf.format(date1);
System.out.println(format);
// 解析字符串,得到Date对象
Date date = sdf.parse(format);
System.out.println(date);
}
}

Calendar 类

  1. 定义:第二代日期类,可以用来表示时间,精确到毫秒
  2. 常用方法
  • getInstance:静态方法,该类的构造器是私有的,只能通过该方法得到Calendar的对象
  • get(Calendar的某个字段):获取该对象的某个日历字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Calendar;

public class Calender01 {
public static void main(String[] args) {
// 1.获取Calendar的对象
Calendar calendar = Calendar.getInstance();
System.out.println(calendar);
// 2.获取日历对象的某个日历字符按
System.out.println("年:" + calendar.get(Calendar.YEAR));
// 月份从0开始
System.out.println("月:" + calendar.get(Calendar.MONTH) + 1);
System.out.println("日:" + calendar.get(Calendar.DAY_OF_MONTH));
System.out.println("小时:" + calendar.get(Calendar.HOUR));
System.out.println("分钟:" + calendar.get(Calendar.MINUTE));
System.out.println("秒:" + calendar.get(Calendar.SECOND));
// 3.Calendar没有专门的格式化方法,故而需要自己拼接
System.out.println(calendar.get(Calendar.YEAR) + "年" + (calendar.get(Calendar.MONTH) + 1) + "月" +
calendar.get(Calendar.DAY_OF_MONTH) + "日");
}
}

LocalDateTime 类

  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
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class LocalDateTime01 {
public static void main(String[] args) {
// 1.获取年月日时分秒
LocalDateTime ldt = LocalDateTime.now();
// 只能获取年月日
LocalDate ld = LocalDate.now();
// 只能获取时分秒
LocalTime lt = LocalTime.now();
// 2.输出年月日时分秒
System.out.println("年=" + ldt.getYear());
System.out.println("年=" + ld.getYear());

System.out.println("月=" + ldt.getMonth());
System.out.println("月=" + ld.getMonth());
System.out.println("月=" + ldt.getMonthValue());
System.out.println("月=" + ld.getMonthValue());

System.out.println("日=" + ld.getDayOfMonth());
System.out.println("日=" + ld.getDayOfMonth());

System.out.println("时=" + ldt.getHour());
System.out.println("时=" + lt.getHour());

System.out.println("分=" + ldt.getMinute());
System.out.println("分=" + lt.getMinute());

System.out.println("秒=" + ldt.getSecond());
System.out.println("秒=" + lt.getSecond());
}
}
  1. DateTimeFormatter 类:用于格式化LocalDateTime对象的时间输出格式
  • ofPattern(字符串):静态方法,通过字符串创建格式化对象
  • format:对日期对象进行格式化,返回字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

/**
* @author 奥数定理
* @version 1.0
*/
public class LocalDateTime01 {
public static void main(String[] args) {
// 1.获取年月日时分秒
LocalDateTime ldt = LocalDateTime.now();
// 只能获取年月日
LocalDate ld = LocalDate.now();
// 只能获取时分秒
LocalTime lt = LocalTime.now();
// 2.格式化日期输出格式
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E");
String format = dateTimeFormatter.format(ldt);
System.out.println(format);
}
}
  1. Instant 类:时间戳类,提供了一系列和Date类转换的方法
  • now:静态方法,获取当前时间的时间戳
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Date;
import java.time.Instant;

public class Instant01 {
public static void main(String[] args) {
// 1.获取Instant对象
Instant instant = Instant.now();
System.out.println(instant);
// 2.将时间戳对象转换成Date对象
Date date = Date.from(instant);
// 3.将Date对象转换成时间戳对象
Instant instant1 = date.toInstant();
}
}

main 方法

  1. JVM调用main方法,所以该方法的访问权限必须是public
  2. JVM调用main方法时,不需要创建对象,故而该方法必须是static修饰的静态方法
  3. 该方法接收String类型的数组参数,该数组中保存直线Java命令时传递给所运行的类的参数

命令执行:java 执行的程序 参数1 参数2 参数3

  1. main方法可以直接访问本类的静态成员

集合

集合的框架体系

集合分为两种:单列集合和双列集合

  • 单列集合:集合中每个节点用于存放一个元素
  • 双列集合:集合中每个节点用于存放两个元素

Collection

  1. 基本介绍
  • Collection 实现子类可以存放多个元素,每个元素可以是Object
  • 有些Collection的实现类,可以存放重复的元素,有些不可以
  • 有些Collection的实现类,有些是有序的(List),有些不是有序(Set)
  • Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
  1. 常用方法
  • add:添加单个元素
  • remove:删除指定元素
    • remove(int index):通过下标索引删除集合中的元素,返回删除的Obejct对象
    • remove(Object):删除集合中的指定元素,返回boolean类型
  • contains:查找元素是否存在
  • size:获取元素个数
  • isEmpty:判断是否为空
  • clear:清空元素
  • addAll(Collection C):添加多个元素
  • containsAll(Collection C):查找多个元素是否都存在
  • removeAll(Collection C):删除多个元素

使用迭代器遍历Collection中的元素

  1. 基本介绍
  • Iterator 对象称为迭代器,主要用于遍历 Collection 集合中的元素
  • 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即返回一个迭代器
  • Iterator 仅用于遍历集合,Iterator 本身不存放对象
  1. Iterator 接口中常用方法
  • hasNext():用于判断当前迭代器指针指向的下一个元素是否存在,存在返回true,不存在返回false
  • next():用于将当前迭代器指针先下移,后返回当前迭代器指针指向的元素
  1. 迭代器底层原理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.*;
import java.util.function.Consumer;

public class Test {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 1.创建实现Collection接口的对象
Collection list = new ArrayList();
// 2.添加元素
list.add("java");
list.add("php");
list.add("python");
list.add("jack");
/*
(1)进入iterator方法,获取迭代器对象
public Iterator<E> iterator() {
// 返回ArrayList的局部内部类Itr的对象
return new Itr();
}
(2)进入ArrayList的局部内部类Itr的无参构造方法,会在底层得到该内部类的cursor、lastRet、expectedModCount属性
private class Itr implements Iterator<E> {
// 游标,用来记录迭代器指向数组的哪个元素(实际是数组下标)
int cursor;
// 游标的上一个索引
int lastRet = -1;
// 记录遍历集合之前,集合已经被修改的次数(当调用集合的remove和add方法时都会使modCount自增)
int expectedModCount = modCount;

Itr() {}

// 方法功能:用于判断游标是否到达集合底层数组的最后一个元素的后面
public boolean hasNext() {
// 如果没有到达集合底层数组的最后一个元素的后面,则返回true
// 否则返回false
return cursor != size;
}

// 方法功能:获取当前游标指向的数组元素,并使游标下移,lastRet下移
public E next() {
// 如果遍历过程中,出现了修改次数不一致的问题,会报异常,停止遍历集合
checkForComodification();
// 记录当前游标
int i = cursor;
// 如果游标大于或者等于了集合元素总个数,则报异常
if (i >= size)
throw new NoSuchElementException();
// 获取外部类的数组(即集合的底层数组)
Object[] elementData = ArrayList.this.elementData;
// 如果游标大于或者等于了数组长度,则报异常
if (i >= elementData.length)
throw new ConcurrentModificationException();
// 使游标自增
cursor = i + 1;
// 获取游标指向元素的上一个元素,并将当前元素的下标赋值给lastRet
return (E) elementData[lastRet = i];
}

// 方法功能:用于判断在遍历集合的过程中,是否有其他线程对集合元素进行修改
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
*/
Iterator iterator = list.iterator();
// 4.遍历元素
while(iterator.hasNext()) {
Object next = iterator.next();
System.out.println("object=" + next);
}
}
}

  1. 使用迭代器遍历的语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class List01 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 1.创建实现Collection接口的对象
Collection list = new ArrayList();
// 2.添加元素
list.add(new Person("zhangsan", 18, "男"));
list.add(new Person("lisi", 18, "男"));
list.add(new Person("wangwu", 18, "男"));
list.add(new Person("zhaoliu", 18, "男"));
list.add("jack");
// 3.返回实现了Iterator接口的对象,即迭代器
Iterator iterator = list.iterator();
// 4.遍历元素
while(iterator.hasNext()) { //判断迭代器指针的下一个元素是否存在
Object next = iterator.next();
System.out.println("object=" + next);
}
// 5.当迭代器指针指向集合中的最后一个元素时,再次执行next方法会报NoSuchElementException异常
// iterator.next();
// 6.故而如果需要再次使用迭代遍历集合,应该重置迭代器
iterator = list.iterator();
while(iterator.hasNext()) { //判断迭代器指针的下一个元素是否存在
Object next = iterator.next();
System.out.println("object=" + next);
}
}
}

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

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}

增强for循环遍历Collection中的元素

增强for的底层还是用得迭代器进行遍历,只是语法上比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.itcz;

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

/**
* @author 奥数定理
* @version 1.0
*/
public class List02 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
// 1.创建实现Collection接口的对象
Collection list = new ArrayList();
// 2.添加元素
list.add(new Person1("zhangsan", 18, "男"));
list.add(new Person1("lisi", 18, "男"));
list.add(new Person1("wangwu", 18, "男"));
list.add(new Person1("zhaoliu", 18, "男"));
list.add("jack");
// 3.使用增强for进行遍历
for (Object o : list) {
System.out.println("object=" + o);
}

}
}

class Person1 {
private String name;
private int age;
private String gender;

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}

利用Lambda表达式遍历Collection中的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

/**
* @author 奥数定理
* @version 1.0
*/
public class List10 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
List list = new ArrayList();
list.add("String");
list.add("java");
list.add(1);
// 1.使用forEach方法进行遍历
// forEach方法底层原理分析:
// 底层是通过一个普通的for循环,拿到集合中的每一个元素
// 并将每一个元素传递给accept方法
// 故而Object对象引用o表示的是集合中的每一个元素
list.forEach(new Consumer() {
@Override
public void accept(Object o) {
System.out.println(o);
}
});
System.out.println("=====使用Lambda表达式遍历集合元素=======");
// 2.使用Lambda表达式遍历集合元素
list.forEach(o -> System.out.println(o));
}
}

List 接口

  1. 基本介绍
  • List接口是Collection接口的实现类(子接口)
  • List集合中的元素有序(即添加顺序和取出顺序一致)、且可重复
  • List有序,故而支持索引
  • 常用的实现类有:ArrayList、LinkedList、Vector
  1. 常用方法
  • void add(int index, Object ele):在index位置插入ele元素,原来index位置之后的元素(包括index所在的元素)依次后移
  • boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来,原来index位置之后的元素(包括index所在的元素)依次后移
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中最后出现的位置
  • Object remove(int index):一移除指定index位置的元素,并返回此元素
  • Object set(int index, Object ele):设置指定index位置的元素为ele,相当于替换
  • List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合,左闭右开

还拥有Collection接口的所有方法,因为List接口是Collection接口的子接口

  1. 细节
  • ArrayList 可以加入空对象:null
  • ArrayList 是由数组来实现数据存储的
  • ArrayList 基本等同于 Vector,但是 ArrayList 是线程不安全的,执行效率比较高

ArrayList 底层结构和源码分析

  1. 底层结构和扩容机制
  • ArrayList中维护了一个Object类型的数据elmentData。transient Object[] elementData; 其中transient修饰的属性不会被序列化
  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第一次添加元素,则扩容elementData为10,如需再次扩容,则扩容elementData为原来的1.5倍
  • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为原来的1.5倍
  • 当一次添加多个元素,并且添加后的总元素个数大于之前数组的长度的1.5倍,此时扩容后的数组大小为添加后的总元素个数
  1. 无参构造器创建的ArrayList进行扩容的底层源码剖析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
import java.util.ArrayList;
import java.util.List;

public class List05 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
ArrayList中的默认空数组属性
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
ArrayList的无参构造方法如下:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
故而执行完成后会在底层创建一个大小为0的Object数组
*/
List list = new ArrayList();
// 2.第一次扩容会将数组大小扩容到10
/*
(1)先将整型自动装箱为Integer类型
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
(2)执行ArrayList的add方法,首先确认elementData的数组大小,其中size是ArraList的属性,默认为0,
故而ensureCapacityInternal(size + 1)方法中传入的参数为1
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
(3)进入ensureCapacityInternal方法,其中先执行calculateCapacity方法,后执行ensureExplicitCapacity方法,进行扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
(4)进入calculateCapacity方法,传入的参数分为为空数组elementData和当前所需最小容量minCapacity:1
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 第一次扩容,应该返回数组扩容的默认大小10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// DEFAULT_CAPACITY是ArrayList的属性,表示数组的默认容量为10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
(5)进入ensureExplicitCapacity方法进行扩容处理,传入的参数为数组的默认扩容大小10
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 为了防止多线程修改集合元素,故而使用modCount属性限制多线程操作

// 此时需要的最小容量变为10,10 - 0 = 10 > 0,正式进入扩容操作grow方法
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
(6)进入grow方法
private void grow(int minCapacity) {
// 记录当前数据的大小
int oldCapacity = elementData.length;
// 计算扩容后数组的大小为原数组大小的1.5倍(原数组大小 + 原数组大小 / 2),由于是无参创建,所以计算后的数组大小仍然为0
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 该语句用于判断是否为无参构造器创建的数组,来让第一次扩容不为1.5倍扩容,而是执行将数组大小扩容为10
if (newCapacity - minCapacity < 0)
// 此时执行该语句,让扩容后的数组大小为10
newCapacity = minCapacity;
// 该语句用于判断扩容后的数组大小是否超过了规定最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原数组内容拷贝到扩容后的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
(7)继续执行add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 执行elementData[0] = 0后,使size的值变为1
elementData[size++] = e;
// 返回插入成功的信息
return true;
}
(8)继续将整型自动装箱为Integer类型,其值为1
(9)执行add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
(10)进入ensureCapacityInternal方法(用于确认数组的容量大小是否可以存储数据),传入参数为2
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
(11)进入calculateCapacity方法(用于计算当前数组储存数据所需的最小容量),传入的参数分为为数组elementData(其大小为10,内容有0)和当前所需最小容量minCapacity:2
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 由于数组不是空数组,直接返回当前所需最小容量minCapacity:2
return minCapacity;
}
(12)进入ensureExplicitCapacity方法进行扩容处理,传入的参数为当前所需最小容量minCapacity:2
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 为了防止多线程修改集合元素,故而使用modCount属性限制多线程操作

// 此时需要的最小容量为2,2 - 10 = -8 < 0,故而不需要扩容处理
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
(13)继续执行add方法,将数据1添加到elementData数组中
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 执行elementData[1] = 1后,使size的值变为2
elementData[size++] = e;
return true;
}
(14)依次插入数据,此时都不会进行扩容
*/
for (int i = 0; i < 10; i++) {
list.add(i);
}
/*
(15)进入add方法
public boolean add(E e) {
// 进入ensureCapacityInternal方法,传入参数为11,size值为10
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
(16)进入ensureCapacityInternal方法(用于确认数组的容量大小是否可以存储数据),传入参数为11
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
(17)进入calculateCapacity方法(用于计算当前数组储存数据所需的最小容量),传入的参数分为为数组elementData(其大小为10,内容有0,1,.....,9)和当前所需最小容量minCapacity:11
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 由于数组不是空数组,直接返回当前所需最小容量minCapacity:11
return minCapacity;
}
(18)进入ensureExplicitCapacity方法进行扩容处理,传入的参数为当前所需最小容量minCapacity:11
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 为了防止多线程修改集合元素,故而使用modCount属性限制多线程操作

// 此时需要的最小容量为11, 11 - 10 = -1 < 0,故而需要扩容处理
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
(19)进入grow方法
private void grow(int minCapacity) {
// 记录当前数据的大小为10
int oldCapacity = elementData.length;
// 计算扩容后数组的大小为原数组大小的1.5倍(原数组大小 + 原数组大小 / 2),故而扩容后的数组大小为15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 该语句用于判断是否为无参构造器创建的数组,来让第一次扩容不为1.5倍扩容,而是执行将数组大小扩容为10
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 该语句用于判断扩容后的数组大小是否超过了规定最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原数组内容拷贝到扩容后的数组中,并且让原数组引用指向扩容后的数组在堆中的地址
elementData = Arrays.copyOf(elementData, newCapacity);
}
(20)进入add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 执行elementData[10] = 10后,使size的值变为11
elementData[size++] = e;
return true;
}
(21)依次类推,都不会进行扩容
*/
for (int i = 10; i < 15; i++) {
list.add(i);
}
/*
(22)此时所需最小容量大小为16,超过了数组的大小,会进行1.5倍扩容,故而扩容后的数组大小变为22
*/
list.add(100);
list.add(200);
list.add(300);
}
}
  1. 有参构造器创建的ArrayList进行扩容的底层源码剖析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import java.util.ArrayList;
import java.util.List;

public class List06 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
ArrayList的默认数组参数如下:
private static final Object[] EMPTY_ELEMENTDATA = {};
ArrayList的有参构造方法如下:
public ArrayList(int initialCapacity) {
// initialCapacity = 8 > 0
if (initialCapacity > 0) {
// 在底层创建一个大小为8的Obejct数组,数组名为elementData
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
*/
List list = new ArrayList(8);
/*
由于最开始数组大小为8,故而可以存储前8个数据的时候都不会进行扩容,以下是存储第九个数据的过程
(1)先将整型自动装箱为Integer类型
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
(2)执行ArrayList的add方法,首先确认elementData的数组大小,其中size是ArraList的属性,由于已经装入了8个数据,所以size变为8
故而ensureCapacityInternal(size + 1)方法中传入的参数为9
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
(3)进入ensureCapacityInternal方法,其中先执行calculateCapacity方法,后执行ensureExplicitCapacity方法,进行扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
(4)进入calculateCapacity方法,传入的参数分为为数组elementData和当前所需最小容量minCapacity:9
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 由于数组不是大小为0的空数组,故而直接返回所需要的最小容量
return minCapacity;
}
(5)进入ensureExplicitCapacity方法进行扩容处理,传入的参数为所需的最小容量:9
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 为了防止多线程修改集合元素,故而使用modCount属性限制多线程操作

// 此时需要的最小容量为9,9 - 8 = 1 > 0,正式进入扩容操作grow方法
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
(6)进入grow方法
private void grow(int minCapacity) {
// 记录当前数据的大小为8
int oldCapacity = elementData.length;
// 计算扩容后数组的大小为原数组大小的1.5倍(原数组大小 + 原数组大小 / 2),所以扩容后的数组大小为12
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 该语句用于判断是否为无参构造器创建的数组,来让第一次扩容不为1.5倍扩容,而是执行将数组大小扩容为10
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 该语句用于判断扩容后的数组大小是否超过了规定最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 将原数组内容拷贝到扩容后的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
(7)继续执行add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1);
// 执行elementData[8] = 8后,使size的值变为9
elementData[size++] = e;
// 返回插入成功的信息
return true;
}
(8)依次插入数据,此时都不会进行扩容
*/
for (int i = 0; i < 10; i++) {
list.add(i);
}
/*
(9)存入12个数据之前都不会进行扩容处理,当存入第13个数据时,会进行扩容处理,此时扩容后的数据大小为18
*/
for (int i = 10; i < 15; i++) {
list.add(i);
}
/*
后续存储数据都不会进行扩容
*/
list.add(100);
list.add(200);
list.add(300);
}
}
  1. 一次添加多个元素,并且添加后的总元素个数大于之前数组的长度的1.5倍的底层源码剖析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import java.util.ArrayList;
import java.util.List;

public class List11 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
ArrayList中的默认空数组属性
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
ArrayList的无参构造方法如下:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
故而执行完成后会在底层创建一个大小为0的Object数组
*/
List list1 = new ArrayList();
List list2 = new ArrayList();
for (int i = 0; i < 20; i++) {
list2.add(i);
}
// list1集合第一次扩容,会将底层element数组大小扩容到10,并将Integer类型的数据1存入集合中
list1.add(0);
/*
一次存入20个数据,添加后的总元素个数为21大于之前数组的长度的1.5倍(即15)
(1)进入addAll方法
public boolean addAll(Collection<? extends E> c) {
// 将集合转为数组
Object[] a = c.toArray();
// 获取要添加的元素个数为20
int numNew = a.length;
// 执行ensureCapacityInternal方法,传入参数为1 + 20 = 21
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
(2)执行ensureCapacityInternal方法,传入参数为1 + 20 = 21
private void ensureCapacityInternal(int minCapacity) {
// 执行calculateCapacity方法,传入参数为原数组,所需最小容量21
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
(3)执行calculateCapacity方法,传入参数为原数组,所需最小容量21
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 不是数组不为{},故而不是第一次扩容,不进入if语句
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 返回所需最小容量21
return minCapacity;
}
(4)执行ensureExplicitCapacity,传入参数为所需最小容量21
private void ensureExplicitCapacity(int minCapacity) {
// 防止多线程修改集合
modCount++;

// 所需最小容量21 - 数组长度10 = 11 > 0,进入if语句
if (minCapacity - elementData.length > 0)
// 执行grow方法,传入参数为所需最小容量21,进行扩容处理
grow(minCapacity);
}
(5)执行grow方法,传入参数为所需最小容量21,进行扩容处理
private void grow(int minCapacity) {
// 记录旧容量,即数组长度
int oldCapacity = elementData.length;
// 记录新容量为原数组长度的1.5倍,即15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 由于新容量15 - 所需最小容量21 = -6 < 0,进入if语句
if (newCapacity - minCapacity < 0)
// 将新容量改为所需最小容量21
newCapacity = minCapacity;
// 新容量未超过数组最大长度,不进入if语句
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
(6)继续执行addAll方法
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
// 拷贝数组,arraycopy方法的参数解释如下:
// 第一个参数:原数组
// 第二个参数:从原数组的哪个位置开始拷贝
// 第三个参数:目标数组
// 第四个参数:拷贝的元素从目标的数组的哪个索引开始赋值
// 第五个参数:拷贝的数据个数
System.arraycopy(a, 0, elementData, size, numNew);
// 将数组中元素个数改为添加数据后的总个数
size += numNew;
// 返回添加成功的标记:true
return numNew != 0;
}
*/
list1.addAll(list2);
}
}

Vector 底层结构和源码分析

  1. 基本介绍
  • Vector 底层也是用的一个对象数组存储数据,protected Object[] elementData;
  • Vector 是线程同步的,即线程安全的,Vector类的操作方法带有synchronized关键字,如果开发中涉及线程安全,推荐使用Vector
  1. 扩容机制
  • 当创建Vector对象时,如果使用的是无参构造器,则初始化elementData容量为10,如需扩容,则扩容elementData为原来的2倍
  • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为原来的2倍
  • 还可以通过构造器指定每次扩容大小
  1. 无参构造器创建的Vector进行扩容的底层源码剖析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import java.util.List;
import java.util.Vector;

public class List07 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
(1)Vector的无参构造器如下:
public Vector() {
this(10);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
因此会在底层创建一个数组大小为10的Object数组,数组名为elementData
*/
List list = new Vector();
/*
(2)由于存储的数据量未超过数组大小,故而不会进行扩容处理
*/
for (int i = 1; i <= 10; i++) {
list.add(i);
}
/*
(3)此时存储第11个数据
public synchronized boolean add(E e) {
modCount++; // 为了防止多线程同时修改数据
// 确认当前数组容量
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
(4)进入ensureCapacityHelper方法,确认当前数组容量,传入参数为11,即所需最小容量为11
private void ensureCapacityHelper(int minCapacity) {
// 判断所需最小容量是否超过了数组的大小,11 - 10 = 1 > 0,故而需要进行扩容处理
if (minCapacity - elementData.length > 0)
// 进入扩容操作
grow(minCapacity);
}
(5)进行扩容处理
private void grow(int minCapacity) {
// 记录当前数组大小为10
int oldCapacity = elementData.length;
// 判断是否指定了扩容大小,如果没有指定则进行2倍扩容,故而扩容后的数组大小为20
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 20 - 11 = 9 > 0,故而不会让扩容后的数组大小等于所需的最小容量大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 未超过最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝原数组内容到扩容后的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
(6)继续执行add方法,添加数据
public synchronized boolean add(E e) {
modCount++; // 为了防止多线程同时修改数据
ensureCapacityHelper(elementCount + 1);
// elementData[10] = 100,elementCount变为11
elementData[elementCount++] = e;
return true;
}
*/
list.add(100);
}
}
  1. 有参构造器创建的Vector进行扩容的底层源码剖析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.itcz;

import java.util.List;
import java.util.Vector;

/**
* @author 奥数定理
* @version 1.0
*/
public class List08 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
(1)Vector的有参构造器如下,可以指定每次扩容多少:
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
因此会在底层创建一个数组大小为10的Object数组,数组名为elementData,并且指定每次扩容是在原有大小的基础上扩容了5个空间
*/
List list = new Vector(10, 5);
/*
(2)由于存储的数据量未超过数组大小,故而不会进行扩容处理
*/
for (int i = 1; i <= 10; i++) {
list.add(i);
}
/*
(3)此时存储第11个数据
public synchronized boolean add(E e) {
modCount++; // 为了防止多线程同时修改数据
// 确认当前数组容量
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
(4)进入ensureCapacityHelper方法,确认当前数组容量,传入参数为11,即所需最小容量为11
private void ensureCapacityHelper(int minCapacity) {
// 判断所需最小容量是否超过了数组的大小,11 - 10 = 1 > 0,故而需要进行扩容处理
if (minCapacity - elementData.length > 0)
// 进入扩容操作
grow(minCapacity);
}
(5)进行扩容处理
private void grow(int minCapacity) {
// 记录当前数组大小为10
int oldCapacity = elementData.length;
// 判断是否指定了扩容大小,如果没有指定则进行2倍扩容,故而扩容后的数组大小为15
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 15 - 11 = 4 > 0,故而不会让扩容后的数组大小等于所需的最小容量大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 未超过最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝原数组内容到扩容后的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
(6)继续执行add方法,添加数据
public synchronized boolean add(E e) {
modCount++; // 为了防止多线程同时修改数据
ensureCapacityHelper(elementCount + 1);
// elementData[10] = 100,elementCount变为11
elementData[elementCount++] = e;
return true;
}
*/
list.add(100);
}
}

LinkedList 底层结构和源码分析

  1. 底层结构
  • LinkedList底层存储数据使用的是双向链表,故而不需要进行扩容处理
  • LinkedList也是线程不安全的集合

LinkedList 查询数据慢,增删数据块,同时操作首尾元素速度也是极快的

  1. 常用方法
特有方法 说明
public void addFirst(E e) 在该列表开头插入指定元素
public void addLast(E e) 将指定元素追加到此列表的末尾
public E getFirst() 返回该列表中的第一个元素
public E getLast() 返回该列表中的最后一个元素
public E removeFirst() 从此列表中删除并返回第一个元素
public E removeLast() 从此列表中删除并返回最后一个元素
  1. 常用方法的源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import java.util.Iterator;
import java.util.LinkedList;

/**
* @author 奥数定理
* @version 1.0
*/
public class List09 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
/*
(1)执行add方法,传入的参数为Integer类型的1(已自动装箱)
public boolean add(E e) {
linkLast(e);
return true;
}
(2)执行linkLast方法(用于将数据添加到链表的最后一个),传入的参数为Integer类型的1(已自动装箱)
void linkLast(E e) {
// 让结点l指向双向链表的尾结点
final Node<E> l = last;
// 创建一个新结点 newNode,前驱指针指向结点l,后继指针指向null,数据域存储Integer类型的1
// 以下是LinkedList类中的局部内部类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;
// }
// }
final Node<E> newNode = new Node<>(l, e, null);
// 尾结点指向新创建的结点
last = newNode;
if (l == null)
// 如果是第一次添加数据,则让首结点也指向新结点
first = newNode;
else
// 如果不是第一次添加数据,则让没有添加结点时的链表尾结点的后继节点指向新结点
l.next = newNode;
// 链表长度加一
size++;
// 为了防止多线程同时操作集合元素
modCount++;
}
*/
System.out.println("添加数据");
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("list=" + linkedList);
System.out.println("删除集合中的第一个元素");
/*
(1)执行remove()方法,底层执行removeFirst方法
public E remove() {
return removeFirst();
}
(2)执行removeFirst方法
public E removeFirst() {
// 创建结点f,并让其指向首结点
final Node<E> f = first;
if (f == null)
// 如果首结点为null,说明集合中没有元素,不能删除
throw new NoSuchElementException();
return unlinkFirst(f);
}
(3)执行unlinkFirst方法,传入参数为链表的首结点
private E unlinkFirst(Node<E> f) {
// 保存首结点的数据域的数据
final E element = f.item;
// 创建新结点 next,并让其指向首结点的下一个结点
final Node<E> next = f.next;
// 将首结点的数据域置为null
f.item = null;
// 使首结点的后继指针指向null,即断开首结点指向第二个结点的链路
f.next = null; // help GC
// 将原链表的第二个结点改为首结点
first = next;
if (next == null)
// 如果原链表中只有一个元素,则删除元素后,让尾结点指向null
last = null;
else
// 如果原链表中不只有一个元素,则删除元素后,让原链表的第二个结点的前驱指针指向null
next.prev = null;
// 集合个数 - 1
size--;
// 为了防止多线程同时操作集合元素
modCount++;
// 返回删除结点数据域的数据
return element;
}
*/
linkedList.remove();
System.out.println("list=" + linkedList);
System.out.println("修改集合中的指定元素");
/*
(1)执行set方法,传入参数分别为索引1和Integer类型的数据999(已自动装箱)
public E set(int index, E element) {
checkElementIndex(index);
//
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

// 该方法用于判断索引是否处于正常范围,不处于则报异常
private void checkElementIndex(int index) {
// 判断索引是否处于正常范围,不处于为true,处于则为false
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

// 该方法用于判断索引是否处于正常范围,不处于返回false,处于返回true
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
(2)执行node方法(用于找到索引所在的结点),最后返回索引所在结点
Node<E> node(int index) {
// 如果索引小于链表长度的一半,则首结点依次向后查找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
// 返回链表中的第二个结点
return x;
// 如果索引大于等于链表长度的一半,则尾结点依次向前查找
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
(3)继续执行set方法
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
// 记录索引所在结点的数据域的数据
E oldVal = x.item;
// 修改索引所在结点的数据域的数据为Integer类型的数据999
x.item = element;
// 返回修改前的元素
return oldVal;
}
*/
linkedList.set(1, 999);
System.out.println("list=" + linkedList);
System.out.println("====使用增强for遍历集合====");
for (Object o : linkedList) {
System.out.println(o);
}
System.out.println("====使用普通for遍历集合=====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
System.out.println("====使用迭代器遍历集合====");
Iterator iterator = linkedList.iterator();
while(iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

ArrayList 和 LinkedList 比较

**** 底层结构 增删的效率 改查的效率
ArrayList 可变数组 较低,数组扩容 较高
LinkedList 双向链表 较高,通过链表追加 较低
  • 如果改查的操作多,选择ArrayList
  • 如果增删的操作多,选择LinkedList

Set 接口

  1. 基本介绍
  • Set 集合中的元素是无序的(添加和取出的顺序不一致),没有索引
  • 不允许重复元素,所以最多包含一个null
  • 但是取出的顺序是固定的,即只要数据存入了,那么它的位置不会发生改变
  1. 常用方法
  • remove(Objec obj):删除指定元素
  • add(Object obj):添加元素

还拥有Collection接口的所有方法,因为Set接口是Collection接口的子接口

  1. 遍历方式
  • 通过迭代器进行遍历
  • 通过增强for循环遍历

不能通过索引进行遍历,因为Set接口不提供get方法

HashSet

  1. 基本介绍
  • HashSet 类 实现了 Set 接口
  • HashSet 底层实际上是 HashMap,而HashMap的底层存储数据用的是数组 + 链表的存储结构(类似邻接表)
1
2
3
Public HashSet() {
map = new hashMap<>();
}
  1. 底层数据结构
  • 链表结点Node定义如下,它是HashMap的静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
  • 数组table的定义如下,默认是null,它是HashMap的属性
1
transient Node<K,V>[] table;
  1. 存储数据时的扩容机制
  • 添加元素时,如果table数组为null,则会进行第一次扩容,此时数组长度会扩容到16个,并设置数组阈值为12(16 * 0.75),如果不为null,会通过哈希算法得到一个hash值,再将数组table长度 - 1 与哈希值进行按位与运算得到索引
  • 得到索引后会通过索引找到该索引在数组table中的位置,判断该数组位置上是否有元素
  • 如果没有,则直接添加到数组中,如果有,则调用equals方法进行比较,如果相同就放弃添加,如果不同,则添加到链表最后
  • 添加成功后会将存储元素总个数 + 1,并对比总个数 和 数组的阈值,超过了会对数组table进行扩容,扩容后的数组长度为原来的两倍,并将数组阈值设置为原来的两倍
  • 如果旧数组不为null,则会将旧数组的元素按照下列规则拷贝到新数组中
    • 如果旧数组元素为单个结点(即链表中只有一个结点),则按照hash值与新数组长度 - 1按位与得到该元素在新数组的索引值
    • 如果就数组元素类型为红黑树,则按照红黑树的规则进行拆分红黑树
    • 如果数组元素不为单个结点(即链表中不止一个结点),此时会通过计算链表中每个结点的hash值与旧数组的长度进行按位与运算的结果,判断该结果是否为0
      • 为0,则将该元素添加到低位链表(新创建)的最后
      • 不为0,则将该元素添加到高位链表(新创建)的最后
- 最后将低位链表添加到新数组[旧索引],高位链表添加到新数组[旧索引 + 旧数组长度]
  • 在Java8中,插入元素后,如果一条链表的长度大于了TREEIFY_THRESHOLD(默认为8),并且数组table的大小大于等于了MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
  1. 添加元素时源码分析
  • HashSet类型的对象第一次添加数据的源码分析如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
import java.util.HashSet;

public class Set02 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
HashSet无参构造器在底层创建了一个HashMap对象
public HashSet() {
map = new HashMap<>();
}
继续执行HashMap的无参构造器,此时会设置加载因子为0.75
DEFAULT_LOAD_FACTOR 为HashMap的属性,默认为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
其中map为HashSet底层维护的一个HashMap类型的对象引用
private transient HashMap<E,Object> map;
*/
HashSet hashSet = new HashSet();
/*
(1)执行add方法,传入的参数为字符串常量:"java"
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object();
(2)执行HashMep类中的put方法,传入的参数分别为字符串常量:"java"和Object类型的对象引用PRESENT(在HashSet类中定义的属性),只是用来在HashMap类中占位
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
(3)执行HashMep类中的hash方法(通过要存入的数据计算该数据应该存放在底层table数组的索引),传入的参数为要存入的数据:"java"
static final int hash(Object key) {
// 用来记录该key值的hashCode值
int h;
// 如果key值为null,则返回0,不为空,返回扰动后的hash值(使原hashCode值的低16位与原hashCode值的高16位进行按位异或,这样可以使最后计算的数组索引分布更均匀)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(4)执行HashMap类的putVal方法,传入的参数分别为 key值:"java"计算后的hash值、key值:"java"、Object类型的对象引用PRESENT、等等
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 下列语句中的table为HashMap底层存储数据的数组。判断数组table是否为null或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
// 如果为null 或者 长度为0,则执行resize方法,即对数组table进行第一次扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(5)执行resize方法,对数组table进行扩容处理
final Node<K,V>[] resize() {
// 记录原来的table数组
Node<K,V>[] oldTab = table;
// 记录原来的table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 记录旧阈值(用于判断数组什么时候进行扩容处理)
int oldThr = threshold;
// 定义两个变量,分别用于记录扩容后的数组长度,扩容后的新阈值
int newCap, newThr = 0;
// 原数组长度为0,不进入if语句
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;
}
// 旧阈值为0,不进入else if语句
else if (oldThr > 0)
newCap = oldThr;
// 进入else语句
else {
// static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 故而第一次扩容后的数组长度为16
newCap = DEFAULT_INITIAL_CAPACITY;
// static final float DEFAULT_LOAD_FACTOR = 0.75f; 故而新阈值为 16 * 0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新阈值不为0,不进入if语句
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 使旧阈值变为新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个扩容后的大小的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将该数组赋值给HashMap底层存储数据的数组table
table = newTab;
// 扩容前的数组为null,故而不需要进行数据拷贝,即不会进入if语句,以下注释只是分析代码
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
// 定义辅助变量
Node<K,V> e;
// 如果数组元素不为空,则进入if语句
if ((e = oldTab[j]) != null) {
// 将旧数组元素置为null,帮助垃圾回收器回收
oldTab[j] = null;
// 如果数组元素中只有单个结点,则通过hash值与新数组长度 - 1按位与得到索引值
// 并将旧数组元素存入到新数组的指定位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果数组元素类型为红黑树类型,则对红黑树进行拆分,以保证树的平衡
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 按照hash值的高位是否为1进行拆分链表
else { // preserve order
// 创建低位链表
Node<K,V> loHead = null, loTail = null;
// 创建高位链表
Node<K,V> hiHead = null, hiTail = null;
// 定义辅助变量,用于记录此时遍历时当前结点的下一个结点
// 因为当前结点会被添加到低位链表/高位链表中,
// 会丢失当前结点的下一个结点
Node<K,V> next;
do {
// 记录此时遍历时当前结点的下一个结点
next = e.next;
// 如果hash值高位为0,进入if语句
if ((e.hash & oldCap) == 0) {
// 判断低位链表是否没有一个结点,是进入if语句
if (loTail == null)
// 将低位链表头指针指向当前结点
loHead = e;
// 不是进入else语句
else
// 将当前遍历结点添加到低位链表最后
loTail.next = e;
// 让尾指针指向低位链表最后一个元素
loTail = e;
}
// 如果hash值高位为1,进入else语句
else {
// 高位链表添加元素原理类似低位链表添加元素
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
// 直到链表遍历完毕
} while ((e = next) != null);
// 如果低位链表不为空,将其添加到指定位置
if (loTail != null) {
// 将低位链表最后一个元素的后继指针置为null
loTail.next = null;
// 将低位链表添加到新数组[原索引]
newTab[j] = loHead;
}
// 如果高位链表不为空,将其添加到指定位置
if (hiTail != null) {
// 将高位链表最后一个元素的后继指针置为null
hiTail.next = null;
// 将高位链表添加到新数组[原索引 + 旧数组长度]
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最后返回扩容后的数组
return newTab;
}
(6)继续执行HashMap类的putVal方法,传入的参数分别为 key值:"java"计算后的hash值、key值:"java"、Object类型的对象引用PRESENT、等等
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 下列语句中的table为HashMap底层存储数据的数组。判断数组table是否为null或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
// 如果为null 或者 长度为0,则执行resize方法,即对数组table进行第一次扩容,并记录扩容后的数组长度
// 注意:因为resize方法中已经让table数组引用指向了扩容后的数组,所以索引tab和table两个数组引用都是指向的扩容后的数组
n = (tab = resize()).length;
// 1.计算该数据应该存放在底层table数组的索引(数组长度和hash值进行逻辑与得到,故而得到的索引值一定在数组长度范围内),并将该索引值赋给变量i,key:"java"计算后的索引值为3
// 2.将该索引的数组元素赋给变量p,并判断该数组元素是否为空,table[3]为null,故而进入if语句
if ((p = tab[i = (n - 1) & hash]) == null)
// 1.创建一个新Node结点(链表中的结点),其数据域的值为"java"和PRESENT,附加数据为hash值(用于判断后续要存入的数据是否已经存入),后继指针为null
// 2.并将该结点赋值给table[3]
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 为了防止多线程同时修改集合元素
++modCount;
// 令底层数据结构中存放元素的个数 + 1(链表和数组上总共存储的元素),再判断是否超过阈值,故而size的值变为1,没有超过阈值12
if (++size > threshold)
resize();
// 该方法可以被HashMap的子类重写用于修改链表的结构,HashMap类中该方法为空方法
afterNodeInsertion(evict);
// 返回null,表示数据已成功存储
return null;
}
(7)由于hashSet对象引用的运行类型为HashSet,通过动态绑定机制,故而会调用HashSet的newNode方法,
传入参数分别为hash值(用于判断后续要存入的数据是否已经存入),key值:"java"、value值:PRESENT,后继指针为null
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
// 执行Node类的有参构造方法
return new Node<>(hash, key, value, next);
}
(8)执行Node类的有参构造方法,传入参数分别为hash值(用于判断后续要存入的数据是否已经存入),key值:"java"、value值:PRESENT,后继指针为null
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

// 将传入的参数分别赋值给Node类的属性
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
*/
hashSet.add("java");
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
}
}
  • HashSet类型的对象第二次添加数据的源码分析如下(索引未重复)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashSet;

public class Set02 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
hashSet.add("java");
/*
和第一次添加数据的执行流程类似,只是不会对数组进行扩容处理,数组索引值为9
故而会创建一个新Node结点(链表中的结点),其数据域的值为"php"和PRESENT,附加数据为hash值(用于判断后续要存入的数据是否已经存入),后继指针为null
并将该结点赋值给table[9]
*/
hashSet.add("php");
hashSet.add("java");
System.out.println("set=" + hashSet);
}
}
  • HashSet类型的对象添加相同元素的源码分析如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import java.util.HashSet;

public class Set02 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
hashSet.add("java");
hashSet.add("php");
/*
(1)执行add方法,传入的参数为字符串常量:"java"
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object();
(2)执行HashMep类中的put方法,传入的参数分别为字符串常量:"java"和Object类型的对象引用PRESENT(在HashSet类中定义的属性),只是用来在HashMap类中占位
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
(3)执行HashMep类中的hash方法(通过要存入的数据计算该数据应该存放在底层table数组的索引),传入的参数为要存入的数据:"java"
static final int hash(Object key) {
// 用来记录该key值的hashCode值
int h;
// 如果key值为null,则返回0,不为空,返回扰动后的hash值(使原hashCode值的低16位与原hashCode值的高16位进行按位异或,这样可以使最后计算的数组索引分布更均匀)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(4)执行HashMap类的putVal方法,传入的参数分别为 key值:"java"计算后的hash值、key值:"java"、Object类型的对象引用PRESENT、等等
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 下列语句中的table为HashMap底层存储数据的数组。判断数组table是否为null或者长度是否为0,因为table数组中已经存入元素,故而不需要扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 1.计算该数据应该存放在底层table数组的索引,并将该索引值赋给变量i,key:"java"计算后的索引值为3
// 2.将该索引的数组元素赋给变量p,并判断该数组元素是否为空,table[3]不为null,故而不进入if语句
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 定义辅助变量
Node<K,V> e; K k;
// 判断table[3]存入的元素(链表的首结点)的hash值与要存入的数据的hash值是否相等
// 并且判断下列条件之一是否为真
// 1. 判断table[3]存入的元素(链表的首结点)与 要存入的数据是否相同
// 2. 判断要存入的数据是否null 并且 调用要存入的数据的equals方法(根据重写后的逻辑判断)和 table[3]存入的元素(链表的首结点)进行对比
// 故而进入if语句
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 结果为真,说明table[3](链表的首结点)存储的元素和要存入的元素一致,即都是字符串常量"java"
// 此时让该结点(链表的首结点)赋值给变量e
e = p;
// 如果链表的首结点为红黑树类型,则调用红黑树的添加元素的方法,但是该table[3](链表的首结点)不为红黑树类型,故而不会进入else if语句
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果上述条件都不成立,说明该数据应该添加到链表最后或者可能链表中其他位置的元素和存入的数据相同,所以不会进入else语句(下列注释只是分析,程序不会进入)
else {
// for 循环,用于记录链表中元素的个数
for (int binCount = 0; ; ++binCount) {
// 先让首结点的下一个结点赋值给变量e(因为前面已经判断了首结点和要存入的元素是否相同),再判断该结点元素是否为null(即已经到了链表最后)
if ((e = p.next) == null) {
// 此时e变量的结点存储的元素为null,直接将要存入的元素加入到链表最后
p.next = newNode(hash, key, value, null);
// 因为binCount是从0开始,故而当链表中结点个数大于或者等于8时,会对链表进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 对链表进行树化,以下是该方法的部分代码
// final void treeifyBin(Node<K,V>[] tab, int hash) {
// int n, index; Node<K,V> e;
// 当数组长度小于64时,不会进行树化,而是对数组进行扩容处理
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// resize();
// ........
// }
treeifyBin(tab, hash);
// 跳出for循环
break;
}
// 判断该结点的元素和要存入的数据是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相同则跳出for循环,并且变量e记录了该结点
break;
// 该语句用于依次遍历链表
p = e;
}
}
// 由于e变量记录的是table[3],故而不为null,进入if语句
if (e != null) { // existing mapping for key
// 记录旧数据的value值,Set集合中所有结点的value值都是PRESENT
V oldValue = e.value;
// 判断是否需要用新值覆盖旧值,onlyIfAbsent默认是false,故而会替换value值
// 但是Set集合中所有结点的value值都是PRESENT,所以无影响
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回旧数据的value值,不为null,故而添加失败
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
*/
hashSet.add("java");
System.out.println("set=" + hashSet);
}
}
  1. HashSet存取顺序不一致的原因

因为遍历HashSet集合是通过依次遍历数组table实现的,先判断数组table中每个元素是否为null,如果为null,直接遍历下一个元素,不为null,则通过该元素依次遍历链表/红黑树,故而取出顺序和存入顺序不一致

  1. HashSet中的元素没有索引的原因

因为HashSet中元素可能是在同一个链表中存储,此时它们在数组中的下标是一样,这就不能通过索引值找到某一个特定元素,故而HashSet取消了索引机制

  1. HashSet利用什么机制保证数据去重的

底层通过hashCode方法和equals方法来对比存入的数据是否一致,如果一致则不会进行添加

  1. 使用迭代器遍历HashSet集合底层源码分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import java.util.HashSet;
import java.util.Iterator;

/**
* @author 奥数定理
* @version 1.0
*/
public class Set07 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
HashSet hashSet = new HashSet();
hashSet.add("java");
hashSet.add("php");
/*
(1)执行HashSet类中的iterator方法
public Iterator<E> iterator() {
return map.keySet().iterator();
}
(2)由于map对象引用的运行类型是HashMap,故而执行HashMap中的keySet方法会返回KeySet对象(懒汉式单例设计模式)
public Set<K> keySet() {
// 1.由于HashMap类继承了AbstractMap类,并且两个类都是在java.util下,所以HashMap中的keySet方法可以访问keySet属性(默认访问权限)
// AbstractMap类中keySet属性的定义:transient Set<K> keySet;
// 2.初始化HashSet对象时,不会初始化keySet属性,即为null
Set<K> ks = keySet;
// 3.所以第一次调用keySet方法时,ks为null,进入if语句,此时才会初始化keySet属性,使其指向KeySet类(HashMap的成员内部类)的对象
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
// 4.返回ks对象引用指向的KeySet对象
return ks;
}
(3)再调用KeySet类(HashMap的成员内部类)的iterator方法,会返回KeyIterator类的对象
final class KeySet extends AbstractSet<K> {
public final Iterator<K> iterator() { return new KeyIterator(); }
}
(4)执行KeyIterator类(HashMap的成员内部类)的无参构造方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
(5)默认会调用其父类HashIterator(HashMap的成员内部类)的无参构造方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 执行该方法会让current置为null,next指向数组table中索引值最小的数组元素,即第一个存在数据的数组元素
HashIterator() {
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 获取HashMap的table数组
Node<K,V>[] t = table;
// 使current置为null
current = next = null;
// 使当前正在遍历的桶索引置为0
index = 0;
// 当数组不为null并且数组长度大于0时,就会进入if语句
if (t != null && size > 0) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
// 该语句用来找到数组table中索引值最小的数组元素,即第一个存在数据的数组元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
}
(6)最终iterator对象引用则是指向KeyIterator对象
*/
Iterator iterator = hashSet.iterator();
/*
(1)由于iterator对象引用则是指向KeyIterator对象(运行类型:KeyIterator),则是执行KeyIterator类的hasNext方法
但是KeyIterator类中没有hasNext方法,故而会去调用其父类HashIterator的hasNext方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 判断下一个结点是否为null,实际上是判断HashMap集合中的元素已经遍历完
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向KeyIterator对象(运行类型:KeyIterator),则是执行KeyIterator类的next方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
(3)由于KeyIterator类继承了HashIterator类,KeyIterator类中又没有nextNode方法,故而执行其父类HashIterator的nextNode方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 该方法用于将当前遍历的结点指针current移动到下一个有元素的结点,下一个要返回的结点指针next移动到下一个有元素的结点
final Node<K,V> nextNode() {
// 定义辅助变量
Node<K,V>[] t;
// 记录下一个要返回的结点指针指向的结点赋值给e
Node<K,V> e = next;
// 防止并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果已经遍历完了最后一个元素,就会进入if语句,报元素不存在的异常
if (e == null)
throw new NoSuchElementException();
// 先将当前遍历的结点指针current移动到下一个有元素的结点,后将下一个要返回的结点指针next移动到下一个有元素的结点,然后判断next是否为空
// 如果不为空,不进入if语句,如果为空,再判断table数组是否为null,如果为null,则不进入if语句,如果不为null,则进入if语句
if ((next = (current = e).next) == null && (t = table) != null) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
do {} while (index < t.length && (next = t[index++]) == null);
}
// 返回当前遍历的结点指针current指向的结点(即单链表结点Node类型)
return e;
}
}
(4)继续执行KeyIterator类的next方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
// 返回单链表结点中的key属性,即key值
public final K next() { return nextNode().key; }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

红黑树
  1. 红黑树的定义(红黑规则)
  • 每个结点要么为黑色,要么为红色
  • 根结点必须为黑色
  • 如果结点没有子结点或者父结点,则该结点对应的相应结点指针为Nil,这些Nil视为叶结点,每个叶结点(Nil)都是黑色的
  • 如果某个结点是红色,那么它的子结点必须是黑色(不能出现两个红色结点相连的情况)
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上均包含相同数目的黑色结点

理解即可,不需要死记硬背

  1. 添加结点后平衡规则:(最开始添加结点的规则就是二叉查找树的添加规则)
画板

注意:添加元素的结点默认为红色

理解即可,不需要死记硬背

LinkedhashSet

  1. 基本介绍
  • LinkedHashSet 是 HashSet 的子类
  • LinkedHashSet 底层是一个 LinkedHashMap 类型的对象,更底层是维护了一个数组 + 双向链表 + 单链表 + 红黑树的数据结构,LinkedHashMap 是 HashMap 的子类
  • LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的
  • LinkedHashSet 不允许添加重复元素
  1. 底层结构
  • 双向链表结点Entry定义如下,它是LinkedHashMap的静态内部类,并且继承了父类HashMap中的静态内部类Node,故而table表中可以存储双向链表的结点
1
2
3
4
5
6
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
  • 数组table的定义如下,默认是null,它是HashMap的属性
1
transient Node<K,V>[] table;
  • 首尾指针定义如下,默认是null,它是LinkedHashMap的属性
1
2
3
4
5
6
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
  1. 存储数据时的扩容机制(无参构造方法创建的对象)
  • 无参构造方法会使底层数组阈值设为16(初始数组长度),加载因子设为0.75(可根据有参构造方法自定义初始数组长度和加载因子)
  • 添加元素时,如果table数组为null,则会进行第一次扩容,此时数组长度会扩容到16个(初始数组长度),并设置数组阈值为12(16 * 0.75),如果不为null,会通过哈希算法得到一个hash值,再将数组table长度 - 1 与哈希值进行按位与运算得到索引
  • 得到索引后会通过索引找到该索引在数组table中的位置,判断该数组位置上是否有元素
  • 如果没有,则直接添加到数组table中并且添加到双向链表的最后,如果有,则调用equals方法进行比较,如果相同就放弃添加,如果不同,则添加到单链表(该索引所在的链表)的最后并添加到双向链表最后
  • 添加成功后会将存储元素总个数 + 1,并对比总个数 和 数组的阈值,超过了会对数组table进行扩容,扩容后的数组长度为原来的两倍,并将数组阈值设置为原来的两倍
  • 如果旧数组不为null,则会将旧数组的元素按照下列规则拷贝到新数组中
    • 如果旧数组元素为单个结点(即单链表中只有一个结点),则按照hash值与新数组长度 - 1按位与得到该元素在新数组的索引值
    • 如果就数组元素类型为红黑树,则按照红黑树的规则进行拆分红黑树
    • 如果数组元素不为单个结点(即单链表中不止一个结点),此时会通过计算单链表中每个元素的hash值与旧数组的长度进行按位与运算的结果,判断该结果是否为0
      • 为0,则将该元素添加到低位链表(新创建)的最后
      • 不为0,则将该元素添加到高位链表(新创建)的最后
- 最后将低位链表添加到新数组[旧索引],高位链表添加到新数组[旧索引 + 旧数组长度]
  • 在Java8中,插入元素后,如果一条链表的长度大于了TREEIFY_THRESHOLD(默认为8),并且数组table的大小大于等于了MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
  1. 添加元素时的底层源码分析
  • 无参构造方法第一次添加数据的源码分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
package com.itcz;

import java.util.LinkedHashSet;

/**
* @author 奥数定理
* @version 1.0
*/
public class Set05 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
(1)调用LinkedHashSet的无参构造方法
public LinkedHashSet() {
super(16, .75f, true);
}
(2)调用LinkedHashSet的父类HashSet的有参构造方法,传入参数主要为数组初始化长度为16,加载因子为0.75
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
(3)调用LinkedHashMap的有参构造方法,传入参数为数组初始化长度为16,加载因子为0.75
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
(4)调用LinkedHashMap的父类HashMap的有参构造方法,传入参数为数组初始化长度为16,加载因子为0.75
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 设置加载因子为0.75
this.loadFactor = loadFactor;
// 设置数组阈值为16,tableSizeFor方法用于保证初始数组长度为2的倍数
this.threshold = tableSizeFor(initialCapacity);
}
*/
LinkedHashSet linkedHashSet = new LinkedHashSet();
/*
(1)由于LinkedHashSet类中没有add方法,故而调用父类HashSet的add方法,传入参数为Integer类型的456
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
(2)调用HashMap的put方法,传入参数为分别为Integer类型的456和Object类型的对象引用PRESENT(在HashSet类中定义的属性),只是用来在HashMap类中占位
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
(3)执行HashMep类中的hash方法(通过要存入的数据计算该数据应该存放在底层table数组的索引),传入的参数为Integer类型的456
static final int hash(Object key) {
// 用来记录该key值的hashCode值
int h;
// 如果key值为null,则返回0,不为空,返回扰动后的hash值(使原hashCode值的低16位与原hashCode值的高16位进行按位异或,这样可以使最后计算的数组索引分布更均匀)
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(4)执行HashMap类的putVal方法,传入的参数分别为 key值:Integer类型的456计算后的hash值、key值:Integer类型的456、Object类型的对象引用PRESENT、等等
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 下列语句中的table为HashMap底层存储数据的数组。判断数组table是否为null或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
// 如果为null 或者 长度为0,则执行resize方法,即对数组table进行第一次扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
(5)执行resize方法,对数组table进行扩容处理
final Node<K,V>[] resize() {
// 记录原来的table数组
Node<K,V>[] oldTab = table;
// 记录原来的table数组的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 记录旧阈值(用于判断数组什么时候进行扩容处理),前面已经设置了阈值为16,故而oldThr的值为16
int oldThr = threshold;
// 定义两个变量,分别用于记录扩容后的数组长度,扩容后的新阈值
int newCap, newThr = 0;
// 原数组长度为0,不进入if语句
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;
}
// 旧阈值为16,进入else if语句
else if (oldThr > 0)
// 设置新容量为旧阈值,即16(即数组初始化长度为16)
newCap = oldThr;
// 不进入else语句
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 新阈值为0,进入if语句
if (newThr == 0) {
// 计算阈值,16 * 0.75 = 12
float ft = (float)newCap * loadFactor;
// 对比新容量和计算后的阈值是否都小于最大数组容量,是则将计算后的阈值赋值给newThr,即newThr的值为12
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 使旧阈值变为新阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 创建一个扩容后的大小的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
// 将该数组赋值给HashMap底层存储数据的数组table
table = newTab;
// 扩容前的数组为null,故而不需要进行数据拷贝,即不会进入if语句,以下注释只是分析代码
if (oldTab != null) {
// 遍历旧数组
for (int j = 0; j < oldCap; ++j) {
// 定义辅助变量
Node<K,V> e;
// 如果数组元素不为空,则进入if语句
if ((e = oldTab[j]) != null) {
// 将旧数组元素置为null,帮助垃圾回收器回收
oldTab[j] = null;
// 如果数组元素中只有单个结点,则通过hash值与新数组长度 - 1按位与得到索引值
// 并将旧数组元素存入到新数组的指定位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果数组元素类型为红黑树类型,则对红黑树进行拆分,以保证树的平衡
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 按照hash值的高位是否为1进行拆分链表
else { // preserve order
// 创建低位链表
Node<K,V> loHead = null, loTail = null;
// 创建高位链表
Node<K,V> hiHead = null, hiTail = null;
// 定义辅助变量,用于记录此时遍历时当前结点的下一个结点
// 因为当前结点会被添加到低位链表/高位链表中,
// 会丢失当前结点的下一个结点
Node<K,V> next;
do {
// 记录此时遍历时当前结点的下一个结点
next = e.next;
// 如果hash值高位为0,进入if语句
if ((e.hash & oldCap) == 0) {
// 判断低位链表是否没有一个结点,是进入if语句
if (loTail == null)
// 将低位链表头指针指向当前结点
loHead = e;
// 不是进入else语句
else
// 将当前遍历结点添加到低位链表最后
loTail.next = e;
// 让尾指针指向低位链表最后一个元素
loTail = e;
}
// 如果hash值高位为1,进入else语句
else {
// 高位链表添加元素原理类似低位链表添加元素
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
// 直到链表遍历完毕
} while ((e = next) != null);
// 如果低位链表不为空,将其添加到指定位置
if (loTail != null) {
// 将低位链表最后一个元素的后继指针置为null
loTail.next = null;
// 将低位链表添加到新数组[原索引]
newTab[j] = loHead;
}
// 如果高位链表不为空,将其添加到指定位置
if (hiTail != null) {
// 将高位链表最后一个元素的后继指针置为null
hiTail.next = null;
// 将高位链表添加到新数组[原索引 + 旧数组长度]
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最后返回扩容后的数组
return newTab;
}
(6)继续执行HashMap类的putVal方法,传入的参数分别为 key值:Integer类型的456计算后的hash值、key值:Integer类型的456、Object类型的对象引用PRESENT、等等
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 下列语句中的table为HashMap底层存储数据的数组。判断数组table是否为null或者长度是否为0
if ((tab = table) == null || (n = tab.length) == 0)
// 如果为null 或者 长度为0,则执行resize方法,即对数组table进行第一次扩容,并记录扩容后的数组长度
// 注意:因为resize方法中已经让table数组引用指向了扩容后的数组,所以索引tab和table两个数组引用都是指向的扩容后的数组
n = (tab = resize()).length;
// 1.计算该数据应该存放在底层table数组的索引(数组长度和hash值进行逻辑与得到,故而得到的索引值一定在数组长度范围内),并将该索引值赋给变量i,key:Integer类型的456计算后的索引值为8
// 2.将该索引的数组元素赋给变量p,并判断该数组元素是否为空,table[8]为null,故而进入if语句
if ((p = tab[i = (n - 1) & hash]) == null)
// 1.创建一个新Node结点(链表中的结点),其数据域的值为Integer类型的456和PRESENT,附加数据为hash值(用于判断后续要存入的数据是否已经存入),后继指针为null
// 2.并将该结点赋值给table[8]
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 为了防止多线程同时修改集合元素
++modCount;
// 令底层数据结构中存放元素的个数 + 1(链表和数组上总共存储的元素),再判断是否超过阈值,故而size的值变为1,没有超过阈值12
if (++size > threshold)
resize();
// 该方法可以被HashMap的子类重写用于修改链表的结构,HashMap类中该方法为空方法
afterNodeInsertion(evict);
// 返回null,表示数据已成功存储
return null;
}
(7)执行newNode方法,因为linkedHashSet对象引用的运行类型是LinkedHashSet,通过动态绑定机制,可知此时调用的是LinkedHashSet的newNode方法
传入参数分别为hash值456(用于判断后续要存入的数据是否已经存入),key值:Integer类型的456和value值:PRESENT,后继指针为null
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 执行LinkedHashMap的静态内部类Entry的有参构造方法,并将创建的Entry类型的对象赋值给对象引用p
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
(8)执行LinkedHashMap的静态内部类Entry的有参构造方法,传入参数分别为hash值456(用于判断后续要存入的数据是否已经存入),key值:Integer类型的456和value值:PRESENT,后继指针为null
static class Entry<K,V> extends HashMap.Node<K,V> {
// 前驱指针和后继指针默认为null
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
// 执行其父类Node(HashMap的静态内部类)的有参构造方法
super(hash, key, value, next);
}
}
(9)执行Entry的父类Node(HashMap的静态内部类)的有参构造方法,传入参数分别为hash值456(用于判断后续要存入的数据是否已经存入),key值:Integer类型的456和value值:PRESENT,后继指针为null
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) {
// 将传入的参数分别赋值给Node类的属性
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
(10)继续执行newNode方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 执行linkNodeLast方法
linkNodeLast(p);
// 返回添加完成后的结点p
return p;
}
(11)执行linkNodeLast方法,传入参数为Entry类型的对象p
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 记录双向链表的尾结点
LinkedHashMap.Entry<K,V> last = tail;
// 让尾指针指向新添加的结点
tail = p;
// 如果尾结点为null,即双向链表第一次添加结点,进入if语句
if (last == null)
// 让头指针也指向新添加的结点
head = p;
// 不是第一次添加结点,则进入else语句,这样就让结点添加到了双向链表的最后
else {
// 让新添加的结点的前驱指针指向原来的双向链表的尾结点
p.before = last;
// 让原来的双向链表的尾结点的后继指针指向新添加的结点
last.after = p;
}
}
*/
linkedHashSet.add(456);
linkedHashSet.add(456);
linkedHashSet.add("Java");
linkedHashSet.add(new Customer1("tom", 1001));
}
}
class Customer1 {
private String name;
private int no;

public Customer1(String name, int no) {
this.name = name;
this.no = no;
}
}
  • 添加重复元素的源码分析:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package com.itcz;

import java.util.LinkedHashSet;

/**
* @author 奥数定理
* @version 1.0
*/
public class Set05 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
/*
(1)调用LinkedHashSet的无参构造方法
public LinkedHashSet() {
super(16, .75f, true);
}
(2)调用LinkedHashSet的父类HashSet的有参构造方法,传入参数主要为数组初始化长度为16,加载因子为0.75
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
(3)调用LinkedHashMap的有参构造方法,传入参数为数组初始化长度为16,加载因子为0.75
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
(4)调用LinkedHashMap的父类HashMap的有参构造方法,传入参数为数组初始化长度为16,加载因子为0.75
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// 设置加载因子为0.75
this.loadFactor = loadFactor;
// 设置数组阈值为16
this.threshold = tableSizeFor(initialCapacity);
}
*/
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(456);
/*
(1)前面执行的方法和上一行代码会执行的方法一致
(2)只是执行putVal方法会不一样
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 数组table不为null并且长度也不为0,故而不进入if语句,不会对数组进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算后得到到的索引值所在的数组元素table[8]已经存在元素,故而不会进入if语句
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 进入else语句
else {
// 定义辅助变量
Node<K,V> e; K k;
// 判断table[8]存入的元素(链表的首结点)的hash值与要存入的数据的hash值是否相等
// 并且判断下列条件之一是否为真
// 1. 判断table[8]存入的元素(链表的首结点)与 要存入的数据是否相同
// 2. 判断要存入的数据是否null 并且 调用要存入的数据的equals方法(根据重写后的逻辑判断)和 table[8]存入的元素(链表的首结点)进行对比
// 故而进入if语句
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果链表的首结点为红黑树类型,则调用红黑树的添加元素的方法,但是该table[8](链表的首结点)不为红黑树类型,故而不会进入else if语句
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 如果上述条件都不成立,说明该数据应该添加到链表最后或者可能链表中其他位置的元素和存入的数据相同,所以不会进入else语句(下列注释只是分析,程序不会进入)
else {
// for 循环,用于记录链表中元素的个数
for (int binCount = 0; ; ++binCount) {
// 先让首结点的下一个结点赋值给变量e(因为前面已经判断了首结点和要存入的元素是否相同),再判断该结点元素是否为null(即已经到了链表最后)
if ((e = p.next) == null) {
// 此时e变量的结点存储的元素为null,直接将要存入的元素加入到单链表最后
p.next = newNode(hash, key, value, null);
// 因为binCount是从0开始,故而当链表中结点个数大于或者等于8时,会对链表进行树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 对链表进行树化,以下是该方法的部分代码
// final void treeifyBin(Node<K,V>[] tab, int hash) {
// int n, index; Node<K,V> e;
// 当数组长度小于64时,不会进行树化,而是对数组进行扩容处理
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
// resize();
// ........
// }
treeifyBin(tab, hash);
// 跳出for循环
break;
}
// 判断该结点的元素和要存入的数据是否相同
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 相同则跳出for循环,并且变量e记录了该结点
break;
// 该语句用于依次遍历链表
p = e;
}
}
// 由于e变量记录的是table[3],故而不为null,进入if语句
if (e != null) { // existing mapping for key
// 记录旧数据的value值,Set集合中所有结点的value值都是PRESENT
V oldValue = e.value;
// 判断是否需要用新值覆盖旧值,onlyIfAbsent默认是false,故而会替换value值
// 但是Set集合中所有结点的value值都是PRESENT,所以无影响
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回旧数据的value值,不为null,故而添加失败
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
*/
linkedHashSet.add(456);
linkedHashSet.add("Java");
linkedHashSet.add(new Customer1("tom", 1001));
}
}
class Customer1 {
private String name;
private int no;

public Customer1(String name, int no) {
this.name = name;
this.no = no;
}
}
  1. LinkedHashSet集合存取数据顺序一致的原因

由于LinkedHashSet存储数据时会调用自己的newNode方法,该方法又会调用linkedNodeLast方法,此方法会将元素添加到双向链表的最后,并使尾指针执行该结点。当遍历集合时,会通过头指针一直遍历到尾指针,此时就实现了存取数据顺序一致

  1. 使用迭代器遍历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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package com.itcz;

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

/**
* @author 奥数定理
* @version 1.0
*/
public class Set08 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add("Java");
linkedHashSet.add("Php");
linkedHashSet.add("JavaScript");
/*
(1)由于LinkedHashSet类中没有iterator方法,故而执行其父类HashSet中的iterator方法
public Iterator<E> iterator() {
return map.keySet().iterator();
}
(2)由于map对象引用的运行类型是ListedHashMap,故而执行LinkedHashMap中的keySet方法会返回LinkedKeySet对象(懒汉式单例设计模式)
public Set<K> keySet() {
// 1.由于LikedHashMap类继承了HashMap类,HashMap类又继承了AbstractMap类,并且三个类都是在java.util下,所以LinkedHashMap中的keySet方法可以访问keySet属性(默认访问权限)
// AbstractMap类中keySet属性的定义:transient Set<K> keySet;
// 2.初始化LinkedHashSet对象时,不会初始化keySet属性,即为null
Set<K> ks = keySet;
// 3.所以第一次调用keySet方法时,ks为null,进入if语句,此时才会初始化keySet属性,使其指向LinkedKeySet类(LinkedHashMap的成员内部类)的对象
if (ks == null) {
ks = new LinkedKeySet();
keySet = ks;
}
// 4.返回ks对象引用指向的LinkedKeySet对象
return ks;
}
(3)再调用LinkedKeySet类(LinkedHashMap的成员内部类)的iterator方法,会返回LinkedKeyIterator类的对象
final class LinkedKeySet extends AbstractSet<K> {
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
}
(4)执行LinkedKeyIterator类(LinkedHashMap的成员内部类)的无参构造方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
(5)默认会调用其父类LinkedHashIterator(LinkedHashMap的成员内部类)的无参构造方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

LinkedHashIterator() {
// 使下一个要返回的结点指针指向双向链表的头结点
next = head;
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 将当前要返回的结点指针置为null
current = null;
}
}
(6)最终iterator对象引用则是指向LinkedKeyIterator对象
*/
Iterator iterator = linkedHashSet.iterator();
/*
(1)由于iterator对象引用则是指向LinkedKeyIterator对象(运行类型:LinkedKeyIterator),则是执行LinkedKeyIterator类的hasNext方法
但是LinkedKeyIterator类中没有hasNext方法,故而会去调用其父类LinkedHashIterator的hasNext方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 判断下一个要返回的结点是否为null,实际上是判断LinkedHashMap集合中的双向链表是否已经遍历完最后一个结点
public final boolean hasNext() {
return next != null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
}
(2)由于iterator对象引用则是指向LinkedKeyIterator对象(运行类型:LinkedKeyIterator),则是执行LinkedKeyIterator类的next方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
(3)由于LinkedKeyIterator类继承了LinkedHashIterator类,LinkedKeyIterator类中又没有nextNode方法,故而执行其父类LinkedHashIterator的nextNode方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 该方法用于先让next和current指针后移,后返回current指向的结点
final LinkedHashMap.Entry<K,V> nextNode() {
// 记录next,防止后移next后找不到前一个结点
LinkedHashMap.Entry<K,V> e = next;
// 判断是否出现了并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 判断当前要返回的结点是否为null,即判断是否已经遍历完双向链表
if (e == null)
throw new NoSuchElementException();
// 使current后移
current = e;
// 使next后移
next = e.after;
// 返回current指向的结点
return e;
}
}

// Entry类(双向链表的结点)为LinkedHashMap类的静态内部类,继承了HashMap类的静态内部类Node(单链表的结点)
static class Entry<K,V> extends HashMap.Node<K,V> {
// 定义双向链表的前驱指针和后继指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
(4)执行Entry类(LinkedHashMap类的静态内部类)的getKey方法,但是Entry没有该方法,故而执行其父类Node(HashMap类的静态内部类)的getKey方法
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值
final K key; // key值
V value; // value值
Node<K,V> next; // 后继指针

// 返回单链表结点中记录的key值
public final K getKey() { return key; }
}
(5)继续执行LinkedKeyIterator类的next方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
// 返回双向链表结点中记录的key值
public final K next() { return nextNode().getKey(); }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

TreeSet

  1. 基本介绍
  • 底层维护的是一个红黑树的数据结构存储数据
  • TreeSet 是 TreeMap 的子类,故而底层存储的还是键值对,只是使用了PRESENT属性对value值进行了占位
  • 存储的元素不重复、无索引、可排序
  • 对于数值类型,默认按照从小到大得顺序进行排序
  • 对于字符、字符串类型,则是按照字符在ASCII码表中得数字升序进行排序
  1. 第一种自定义排序规则来控制TreeSet集合的排序
  • 让类实现Comparable接口,并重写compareTo方法
  • 红黑树会循环调用重写的compareTo方法来找到要存储的结点在红黑树中的位置,并在结点添加后按照平衡规则对红黑树的结构进行调整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import java.util.Iterator;
import java.util.TreeSet;

/**
* @author 奥数定理
* @version 1.0
*/
public class Set10 {
public static void main(String[] args) {
TreeSet<Student> ts = new TreeSet<>();
/*
关键方法分析(该方法是TreeMap的方法)
public V put(K key, V value) {
// 将红黑树根节点赋值给变量t
Entry<K,V> t = root;
// 如果根节点为null,即第一次添加结点,则进入if语句
if (t == null) {
// 判断类型,防止存储数据为null
compare(key, key); // type (and possibly null) check

// 将数据添加到根结点
root = new Entry<>(key, value, null);
// 集合元素个数设为1
size = 1;
// 防止高并发修改集合
modCount++;
// 返回添加成功的标志
return null;
}
// 定义变量
int cmp;
Entry<K,V> parent;
// 区分 comparator(第二种方式自定义排序规则,这个是用构造器上的) and comparable(第一种方式自定义排序规则,这个是用在类上的) 的路径
Comparator<? super K> cpr = comparator;
// 如果cpr不为null,即表示使用构造器中传入的自定义排序规则(优先级最高)进行排序
if (cpr != null) {
do {
// 记录每次遍历红黑树时的根节点
parent = t;
// 调用构造器中传入的实现了Comparator接口的类的compare方法
// 第一个参数为要存储的数据,第二个参数为红黑树存储的数据
cmp = cpr.compare(key, t.key);
// 为负数,对比左子树(故而可以通过重写compare方法,让红黑树左子树结点的值都比根节点大,从而实现从大到小)
if (cmp < 0)
t = t.left;
// 为正数,对比右子树
else if (cmp > 0)
t = t.right;
// 为0,替换vlaue值
else
return t.setValue(value);
// 直到遍历到Nil结点
} while (t != null);
}
// 如果cpr为null,即表示使用存储数据的类中实现的自定义排序规则(优先级较低)进行排序
else {
// 不允许key值为null
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// 向上转型(判断key值的运行类型是否实现了Comparable接口,没有实现则会出现类型转换异常)
Comparable<? super K> k = (Comparable<? super K>) key;
do {
// 记录根节点
parent = t;
// 调用重写的compareTo方法进行对比
cmp = k.compareTo(t.key);
// 为负数,对比左子树
if (cmp < 0)
t = t.left;
// 为正数,对比右子树
else if (cmp > 0)
t = t.right;
// 为0,替换value
else
return t.setValue(value);
直到遍历到Nil结点
} while (t != null);
}
// 根据传入的数据创建红黑树结点
Entry<K,V> e = new Entry<>(key, value, parent);
// 为负数,则插入到对应结点的左子树
if (cmp < 0)
parent.left = e;
// 为正数,则插入到对应结点的右子树
else
parent.right = e;
// 对红黑树进行调整
fixAfterInsertion(e);
// 集合元素个数自增
size++;
// 防止高并发修改集合
modCount++;
// 返回添加成功的标志
return null;
}
*/
ts.add(new Student("zhangsan", 24));
ts.add(new Student("lisi", 23));
ts.add(new Student("wangwu", 22));
for (Student next : ts) {
System.out.println(next);
}
}
}

class Student implements Comparable<Student> {
private String name;
private int age;

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
/*
this:表示当前Student对象
o:表示红黑树中存储的结点
*/
public int compareTo(Student o) {
/*
为负数,则添加到左子树
为正数,则添加到右子树
为0,不会进行添加
*/
return this.getAge() - o.getAge();
}
}
  1. 第二种自定义排序规则来控制TreeSet集合的排序
  • 创建TreeSet对象时,传递实现了比较器Comparator接口的对象
  • 红黑树会循环调用重写的compare方法来找到要存储的结点在红黑树中的位置,并在结点添加后按照平衡规则对红黑树的结构进行调整
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;

/**
* @author 奥数定理
* @version 1.0
*/
public class Set11 {
public static void main(String[] args) {
// 匿名内部类
// TreeSet<String> ts = new TreeSet<>(new Comparator<String>() {
// @Override
// public int compare(String o1, String o2) {
// int i = o1.length() - o2.length();
// i = i == 0 ? o1.compareTo(o2) : i;
// return i;
// }
// });
// lambda表达式
/*
该有参构造器会将匿名内部类对象赋值给TreeMap类的compartor属性
*/
TreeSet<String> ts = new TreeSet<>((o1, o2) -> o1.length() - o2.length() == 0 ? o1.compareTo(o2) : o1.length() - o2.length());
/*
关键方法分析(该方法是TreeMap的方法)
public V put(K key, V value) {
// 将红黑树根节点赋值给变量t
Entry<K,V> t = root;
// 如果根节点为null,即第一次添加结点,则进入if语句
if (t == null) {
// 判断类型,防止存储数据为null
compare(key, key); // type (and possibly null) check

// 将数据添加到根结点
root = new Entry<>(key, value, null);
// 集合元素个数设为1
size = 1;
// 防止高并发修改集合
modCount++;
// 返回添加成功的标志
return null;
}
// 定义变量
int cmp;
Entry<K,V> parent;
// 区分 comparator(第二种方式自定义排序规则,这个是用构造器上的) and comparable(第一种方式自定义排序规则,这个是用在类上的) 的路径
Comparator<? super K> cpr = comparator;
// 如果cpr不为null,即表示使用构造器中传入的自定义排序规则(优先级最高)进行排序
if (cpr != null) {
do {
// 记录每次遍历红黑树时的根节点
parent = t;
// 调用构造器中传入的实现了Comparator接口的类的compare方法
// 第一个参数为要存储的数据,第二个参数为红黑树存储的数据
cmp = cpr.compare(key, t.key);
// 为负数,对比左子树(故而可以通过重写compare方法,让红黑树左子树结点的值都比根节点大,从而实现从大到小)
if (cmp < 0)
t = t.left;
// 为正数,对比右子树
else if (cmp > 0)
t = t.right;
// 为0,替换vlaue值
else
return t.setValue(value);
// 直到遍历到Nil结点
} while (t != null);
}
// 如果cpr为null,即表示使用存储数据的类中实现的自定义排序规则(优先级较低)进行排序
else {
// 不允许key值为null
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// 向上转型(判断key值的运行类型是否实现了Comparable接口,没有实现则会出现类型转换异常)
Comparable<? super K> k = (Comparable<? super K>) key;
do {
// 记录根节点
parent = t;
// 调用重写的compareTo方法进行对比
cmp = k.compareTo(t.key);
// 为负数,对比左子树
if (cmp < 0)
t = t.left;
// 为正数,对比右子树
else if (cmp > 0)
t = t.right;
// 为0,替换value
else
return t.setValue(value);
直到遍历到Nil结点
} while (t != null);
}
// 根据传入的数据创建红黑树结点
Entry<K,V> e = new Entry<>(key, value, parent);
// 为负数,则插入到对应结点的左子树
if (cmp < 0)
parent.left = e;
// 为正数,则插入到对应结点的右子树
else
parent.right = e;
// 对红黑树进行调整
fixAfterInsertion(e);
// 集合元素个数自增
size++;
// 防止高并发修改集合
modCount++;
// 返回添加成功的标志
return null;
}
*/
ts.add("hello");
ts.add("world");
ts.add("a");
ts.add("ab");
ts.add("abc");
for (String str : ts) {
System.out.println(str);
}
}
}

Map

  1. 基本介绍
  • Map 接口用于保存具有映射关系的数据:key-value
  • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap的静态内部类Node对象中
  • Map 中的 key 不可以重复,原因是底层源码中重复的key的数据不会添加到集合中,但是默认会替换旧的value值
  • Map 中 value 可以重复
  • Map 中 key 可以为 null,value 也可以为 null,但是值为 null 的 key 只能有一个,而值为 null 的 value 可以有多个
  • 一般使用 String 类作为 Map 的 key
  • key 与 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
  1. 常用方法
  • put(Object o):添加数据
  • remove(Ojbect key):根据键删除映射关系
  • get(Ojbect key):根据键获取值
  • size():获取元素个数
  • isEmpty():判断元素个数是否为0
  • clear():清空元素
  • containsKey(Object key):查找键是否存在
  • keySet():获取KeySet类的对象,进而可以遍历集合中的每个元素的key
  • entrySet():获取EntrySet类的对象,进而可以遍历集合中每个元素
  • values():获取Values类的对象,进而可以遍历集合中每个元素的value

HashMap

  1. 基本介绍
  • 存储键值对key-value
  • 存取顺序不一致,但是每次遍历集合的顺序都是一致的,因为它是通过遍历数组+链表来取出数据,通过计算hash值在数组中的索引值来存储数据
  • key值唯一,value值可重复,并且都能为null
  1. 底层结构
  • 链表结点Node定义如下,它是HashMap的静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
  • 数组table的定义如下,默认是null,它是HashMap的属性
1
transient Node<K,V>[] table;
  1. 添加数据时的扩容机制
  • HashMap的无参构造方法会设置加载因子为0.75
  • 添加元素时,如果table数组为null,则会进行第一次扩容,此时数组长度会扩容到16个,并设置数组阈值为12(16 * 0.75),如果不为null,会通过哈希算法得到一个hash值,再将数组table长度 - 1 与哈希值进行按位与运算得到索引
  • 得到索引后会通过索引找到该索引在数组table中的位置,判断该数组位置上是否有元素
  • 如果没有,则直接添加到数组中,如果有,则调用equals方法进行比较key值,如果相同就替换value值,如果不同,则添加到链表最后
  • 添加成功后会将存储元素总个数 + 1,并对比总个数 和 数组的阈值,超过了会对数组table进行扩容,扩容后的数组长度为原来的两倍,并将数组阈值设置为原来的两倍
  • 如果旧数组不为null,则会将旧数组的元素按照下列规则拷贝到新数组中
    • 如果旧数组元素为单个结点(即链表中只有一个结点),则按照hash值与新数组长度 - 1按位与得到该元素在新数组的索引值
    • 如果就数组元素类型为红黑树,则按照红黑树的规则进行拆分红黑树
    • 如果数组元素不为单个结点(即链表中不止一个结点),此时会通过计算链表中每个结点的hash值与旧数组的长度进行按位与运算的结果,判断该结果是否为0
      • 为0,则将该元素添加到低位链表(新创建)的最后
      • 不为0,则将该元素添加到高位链表(新创建)的最后
- 最后将低位链表添加到新数组[旧索引],高位链表添加到新数组[旧索引 + 旧数组长度]
  • 在Java8中,插入元素后,如果一条链表的长度大于等于了TREEIFY_THRESHOLD(默认为8),并且数组table的大小大于等于了MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
  1. 使用迭代器遍历HashMap集合的源码分析
  • 使用迭代器获取HashMap中每个单链表结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import java.util.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class Map01 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap<>();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行HashMap的entrySet方法,返回EntrySet类对象(HashMap类的成员内部类)
由于EntrySet类继承了AbstractSet类,而AbstractSet类继承了AbstractCollection类并且实现了Set接口
故而对象引用的编译类型可以是Set接口
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
// 懒汉式单例设计模式
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
*/
Set set = map.entrySet();
/*
(1) 对象引用set的运行类型为EntrySet,故而执行EntrySet类中iterator方法
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
}
(2) 执行EntryIterator类(HashMap类的成员内部类)的无参构造方法
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
(3) 默认会执行EntryIterator类的父类HashIterator的无参构造方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 执行该方法会让current置为null,next指向数组table中索引值最小的数组元素,即第一个存在数据的数组元素
HashIterator() {
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 获取HashMap的table数组
Node<K,V>[] t = table;
// 使current置为null
current = next = null;
// 使当前正在遍历的桶索引置为0
index = 0;
// 当数组不为null并且数组长度大于0时,就会进入if语句
if (t != null && size > 0) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
// 该语句用来找到数组table中索引值最小的数组元素,即第一个存在数据的数组元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
}
(4) 最后对象引用iterator指向EntryIterator对象
*/
Iterator iterator = set.iterator();
/*
(1)由于iterator对象引用则是指向EntryIterator对象(运行类型:EntryIterator),则是执行EntryIterator类的hasNext方法
但是EntryIterator类中没有hasNext方法,故而会去调用其父类HashIterator的hasNext方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 判断下一个结点是否为null,实际上是判断HashMap集合中的元素已经遍历完
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向EntryIterator对象(运行类型:EntryIterator),则是执行EntryIterator类的next方法
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
(3)由于EntryIterator类继承了HashIterator类,EntryIterator类中又没有nextNode方法,故而执行其父类HashIterator的nextNode方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 该方法用于将当前遍历的结点指针current移动到下一个有元素的结点,下一个要返回的结点指针next移动到下一个有元素的结点
final Node<K,V> nextNode() {
// 定义辅助变量
Node<K,V>[] t;
// 记录下一个要返回的结点指针指向的结点赋值给e
Node<K,V> e = next;
// 防止并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果已经遍历完了最后一个元素,就会进入if语句,报元素不存在的异常
if (e == null)
throw new NoSuchElementException();
// 先将当前遍历的结点指针current移动到下一个有元素的结点,后将下一个要返回的结点指针next移动到下一个有元素的结点,然后判断next是否为空
// 如果不为空,不进入if语句,如果为空,再判断table数组是否为null,如果为null,则不进入if语句,如果不为null,则进入if语句
if ((next = (current = e).next) == null && (t = table) != null) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
do {} while (index < t.length && (next = t[index++]) == null);
}
// 返回当前遍历的结点指针current指向的结点(即单链表结点Node类型)
return e;
}
}
(4)继续执行EntryIterator类的next方法
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
// 返回单链表结点
public final Map.Entry<K,V> next() { return nextNode(); }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过HashMap的entrySet方法可以获取到EntrySet(HashMap类的成员内部类)对象,通过该对象可以根据数据删除HashMap中对应的单链表结点,还可以获取EntryIterator(HashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取HashMap中每个单链表结点

  • 使用迭代器获取HashMap中每个单链表结点的key值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package com.itcz;

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

/**
* @author 奥数定理
* @version 1.0
*/
public class Map02 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap<>();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行HashMap的keySet方法,返回KeySet类对象(HashMap类的成员内部类)
由于KeySet类继承了AbstractSet类,而AbstractSet类继承了AbstractCollection类并且实现了Set接口
故而对象引用的编译类型可以是Set接口
public Set<K> keySet() {
// 1.HashMap类又继承了AbstractMap类,并且两个类都是在java.util下,所以HashMap中的keySet方法可以访问keySet属性(默认访问权限)
// AbstractMap类中keySet属性的定义:transient Set<K> keySet;
// 2.初始化HashSet对象时,不会初始化keySet属性,即为null(懒汉式单例设计模式)
Set<K> ks = keySet;
// 3.所以第一次调用keySet方法时,ks为null,进入if语句,此时才会初始化keySet属性,使其指向LinkedKeySet类(LinkedHashMap的成员内部类)的对象
if (ks == null) {
ks = new LinkedKeySet();
keySet = ks;
}
// 4.返回ks对象引用指向的LinkedKeySet对象
return ks;
}
*/
Set set = map.keySet();
/*
(1) 对象引用set的运行类型为KeySet,故而执行KeySet类中iterator方法
final class KeySet extends AbstractSet<K> {
public final Iterator<K> iterator() { return new KeyIterator(); }
}
(2) 执行KeyIterator类(HashMap类的成员内部类)的无参构造方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
(3) 默认会执行EntryIterator类的父类HashIterator的无参构造方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 执行该方法会让current置为null,next指向数组table中索引值最小的数组元素,即第一个存在数据的数组元素
HashIterator() {
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 获取HashMap的table数组
Node<K,V>[] t = table;
// 使current置为null
current = next = null;
// 使当前正在遍历的桶索引置为0
index = 0;
// 当数组不为null并且数组长度大于0时,就会进入if语句
if (t != null && size > 0) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
// 该语句用来找到数组table中索引值最小的数组元素,即第一个存在数据的数组元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
}
(4) 最后对象引用iterator指向EntryIterator对象
*/
Iterator iterator = set.iterator();
/*
(1)由于iterator对象引用则是指向KeyIterator对象(运行类型:KeyIterator),则是执行KeyIterator类的hasNext方法
但是KeyIterator类中没有hasNext方法,故而会去调用其父类HashIterator的hasNext方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 判断下一个结点是否为null,实际上是判断HashMap集合中的元素已经遍历完
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向KeyIterator对象(运行类型:KeyIterator),则是执行KeyIterator类的next方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
(3)由于KeyIterator类继承了HashIterator类,EntryIterator类中又没有nextNode方法,故而执行其父类HashIterator的nextNode方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 该方法用于将当前遍历的结点指针current移动到下一个有元素的结点,下一个要返回的结点指针next移动到下一个有元素的结点
final Node<K,V> nextNode() {
// 定义辅助变量
Node<K,V>[] t;
// 记录下一个要返回的结点指针指向的结点赋值给e
Node<K,V> e = next;
// 防止并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果已经遍历完了最后一个元素,就会进入if语句,报元素不存在的异常
if (e == null)
throw new NoSuchElementException();
// 先将当前遍历的结点指针current移动到下一个有元素的结点,后将下一个要返回的结点指针next移动到下一个有元素的结点,然后判断next是否为空
// 如果不为空,不进入if语句,如果为空,再判断table数组是否为null,如果为null,则不进入if语句,如果不为null,则进入if语句
if ((next = (current = e).next) == null && (t = table) != null) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
do {} while (index < t.length && (next = t[index++]) == null);
}
// 返回当前遍历的结点指针current指向的结点(即单链表结点Node类型)
return e;
}
}
(4)继续执行KeyIterator类的next方法
final class KeyIterator extends HashIterator
implements Iterator<K> {
// 返回单链表结点的key值
public final K next() { return nextNode().key; }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过HashMap的keySet方法可以获取到KeySet(HashMap类的成员内部类)对象,通过该对象可以根据传入的key值删除HashMap中对应的单链表结点,还可以获取KeyIterator(HashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取HashMap中每个单链表结点的key值

  • 使用迭代器获取HashMap中每个单链表结点的value值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import java.util.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class Map03 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new HashMap<>();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行HashMap的values方法,返回Values类对象(HashMap类的成员内部类)
由于Values类继承了AbstractCollection类,而AbstractCollection类继承了Collection接口
故而对象引用的编译类型可以是Collection接口
public Collection<V> values() {
// 1.由于HashMap类继承了AbstractMap类,并且两个类都是在java.util下,所以HashMap中的values方法可以访问values属性(默认访问权限)
// AbstractMap类中values属性的定义:transient Collection<K> values;
// 2.初始化HashMap对象时,不会初始化values属性,即为null(懒汉式单例设计模式)
Collection<V> vs = values;
// 3.所以第一次调用values方法时,vs为null,进入if语句,此时才会初始化values属性,使其指向Values类的对象
if (vs == null) {
vs = new Values();
values = vs;
}
// 4.返回vs对象引用指向的values对象
return vs;
}
*/
Collection collection = map.values();
/*
(1) 对象引用collection的运行类型为Values,故而执行Values类中iterator方法
final class Values extends AbstractCollection<V> {
public final Iterator<V> iterator() { return new ValueIterator(); }
}
(2) 执行ValueIterator类(HashMap类的成员内部类)的无参构造方法
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
(3) 默认会执行EntryIterator类的父类HashIterator的无参构造方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 执行该方法会让current置为null,next指向数组table中索引值最小的数组元素,即第一个存在数据的数组元素
HashIterator() {
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 获取HashMap的table数组
Node<K,V>[] t = table;
// 使current置为null
current = next = null;
// 使当前正在遍历的桶索引置为0
index = 0;
// 当数组不为null并且数组长度大于0时,就会进入if语句
if (t != null && size > 0) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
// 该语句用来找到数组table中索引值最小的数组元素,即第一个存在数据的数组元素
do {} while (index < t.length && (next = t[index++]) == null);
}
}
}
(4) 最后对象引用iterator指向ValueIterator对象
*/
Iterator iterator = collection.iterator();
/*
(1)由于iterator对象引用则是指向ValueIterator对象(运行类型:ValueIterator),则是执行ValueIterator类的hasNext方法
但是ValueIterator类中没有hasNext方法,故而会去调用其父类HashIterator的hasNext方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 判断下一个结点是否为null,实际上是判断HashMap集合中的元素已经遍历完
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向ValueIterator对象(运行类型:ValueIterator),则是执行ValueIterator类的next方法
final class ValueIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().value; }
}
(3)由于ValueIterator类继承了HashIterator类,ValueIterator类中又没有nextNode方法,故而执行其父类HashIterator的nextNode方法
abstract class HashIterator {
Node<K,V> next; // 下一个要返回的节点
Node<K,V> current; // 当前节点(主要用于 remove 操作)
int expectedModCount; // 预期的修改次数(用于并发检查)
int index; // 当前正在遍历的桶索引

// 该方法用于将当前遍历的结点指针current移动到下一个有元素的结点,下一个要返回的结点指针next移动到下一个有元素的结点
final Node<K,V> nextNode() {
// 定义辅助变量
Node<K,V>[] t;
// 记录下一个要返回的结点指针指向的结点赋值给e
Node<K,V> e = next;
// 防止并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 如果已经遍历完了最后一个元素,就会进入if语句,报元素不存在的异常
if (e == null)
throw new NoSuchElementException();
// 先将当前遍历的结点指针current移动到下一个有元素的结点,后将下一个要返回的结点指针next移动到下一个有元素的结点,然后判断next是否为空
// 如果不为空,不进入if语句,如果为空,再判断table数组是否为null,如果为null,则不进入if语句,如果不为null,则进入if语句
if ((next = (current = e).next) == null && (t = table) != null) {
// 判断当前正在遍历的桶索引是否小于数组长度,如果为真,则执行以下逻辑,为假,则跳出循环
// 1.将当前正在遍历的桶索引的数组元素赋值给next,后让当前正在遍历的桶索引自增
// 2.最后判断next是否为null
do {} while (index < t.length && (next = t[index++]) == null);
}
// 返回当前遍历的结点指针current指向的结点(即单链表结点Node类型)
return e;
}
}
(4)继续执行ValueIterator类的next方法
final class ValueIterator extends HashIterator
implements Iterator<K> {
// 返回单链表结点的value值
public final K next() { return nextNode().value; }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过HashMap的values方法可以获取到Values(HashMap类的成员内部类)对象,通过该对象可以根据传入的value值删除HashMap中对应的单链表结点,还可以获取ValueIterator(HashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取HashMap中每个单链表结点的value值

LinkedHashMap

  1. 基本介绍
  • 存储键值对key-value
  • 存取顺序一致,因为底层是用双向链表来控制存取顺序
  • key值唯一,value值可重复,key和value都可以为null
  1. 底层结构
  • 双向链表结点Entry定义如下,它是LinkedHashMap的静态内部类,并且继承了父类HashMap中的静态内部类Node,故而table表中可以存储双向链表的结点
1
2
3
4
5
6
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
  • 数组table的定义如下,默认是null,它是HashMap的属性
1
transient Node<K,V>[] table;
  • 首尾指针定义如下,默认是null,它是LinkedHashMap的属性
1
2
3
4
5
6
transient LinkedHashMap.Entry<K,V> head;

/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
  1. 添加数据时的扩容机制
  • LinkedhashMap的无参构造方法会调用其父类的HashMap的无参构造方法,该方法会将加载因子设为0.75
  • 添加元素时,如果table数组为null,则会进行第一次扩容,此时数组长度会扩容到16个,并设置数组阈值为12(16 * 0.75),如果不为null,会通过哈希算法得到一个hash值,再将数组table长度 - 1 与哈希值进行按位与运算得到索引
  • 得到索引后会通过索引找到该索引在数组table中的位置,判断该数组位置上是否有元素
  • 如果没有,则直接添加到数组table中并且添加到双向链表的最后,如果有,则调用equals方法进行比较key值,如果相同就替换value值,如果不同,则添加到单链表(该索引所在的链表)的最后并添加到双向链表最后
  • 添加成功后会将存储元素总个数 + 1,并对比总个数 和 数组的阈值,超过了会对数组table进行扩容,扩容后的数组长度为原来的两倍,并将数组阈值设置为原来的两倍
  • 如果旧数组不为null,则会将旧数组的元素按照下列规则拷贝到新数组中
    • 如果旧数组元素为单个结点(即单链表中只有一个结点),则按照hash值与新数组长度 - 1按位与得到该元素在新数组的索引值
    • 如果就数组元素类型为红黑树,则按照红黑树的规则进行拆分红黑树
    • 如果数组元素不为单个结点(即单链表中不止一个结点),此时会通过计算单链表中每个元素的hash值与旧数组的长度进行按位与运算的结果,判断该结果是否为0
      • 为0,则将该元素添加到低位链表(新创建)的最后
      • 不为0,则将该元素添加到高位链表(新创建)的最后
- 最后将低位链表添加到新数组[旧索引],高位链表添加到新数组[旧索引 + 旧数组长度]
  • 在Java8中,插入元素后,如果一条链表的长度大于了TREEIFY_THRESHOLD(默认为8),并且数组table的大小大于等于了MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)
  1. 使用迭代器遍历LinkedHashMap集合的源码分析
  • 使用迭代器获取LinkedHashMap中每个双向链表结点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package com.itcz;

import java.util.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class Map04 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new LinkedHashMap();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行LinkedHashMap的entrySet方法,返回LinkedEntrySet类对象(LinkedHashMap类的成员内部类),懒汉式单例设计模式
由于LinkedEntrySet类继承了AbstractSet类,而AbstractSet类继承了AbstractCollection类并且实现了Set接口
故而对象引用的编译类型可以是Set接口
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
*/
Set set = map.entrySet();
/*
(1) 对象引用set的运行类型为LinkedEntrySet,故而执行LinkedEntrySet类中iterator方法
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
}
(2) 执行LinkedEntryIterator类(LinkedHashMap类的成员内部类)的无参构造方法
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
(3) 默认会执行LinkedEntryIterator类的父类LinkedHashIterator的无参构造方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

LinkedHashIterator() {
// 使下一个要返回的结点指针指向双向链表的头结点
next = head;
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 将当前要返回的结点指针置为null
current = null;
}
}
(4) 最后对象引用iterator指向LinkedEntryIterator对象
*/
Iterator iterator = set.iterator();
/*
(1)由于iterator对象引用则是指向LinkedEntryIterator对象(运行类型:LinkedEntryIterator),则是执行LinkedEntryIterator类的hasNext方法
但是LinkedEntryIterator类中没有hasNext方法,故而会去调用其父类LinkedHashIterator的hasNext方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 判断下一个要返回的结点是否为null,实际上是判断LinkedHashMap集合中的双向链表是否已经遍历完最后一个结点
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向LinkedEntryIterator对象(运行类型:LinkedEntryIterator),则是执行LinkedEntryIterator类的next方法
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode() }
}
(3)由于LinkedEntryIterator类继承了LinkedHashIterator类,LinkedEntryIterator类中又没有nextNode方法,故而执行其父类LinkedHashIterator的nextNode方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 该方法用于先让next和current指针后移,后返回current指向的结点
final LinkedHashMap.Entry<K,V> nextNode() {
// 记录next,防止后移next后找不到前一个结点
LinkedHashMap.Entry<K,V> e = next;
// 判断是否出现了并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 判断当前要返回的结点是否为null,即判断是否已经遍历完双向链表
if (e == null)
throw new NoSuchElementException();
// 使current后移
current = e;
// 使next后移
next = e.after;
// 返回current指向的结点
return e;
}
}

// Entry类(双向链表的结点)为LinkedHashMap类的静态内部类,继承了HashMap类的静态内部类Node(单链表的结点)
static class Entry<K,V> extends HashMap.Node<K,V> {
// 定义双向链表的前驱指针和后继指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
(4)继续执行LinkedEntryIterator类的next方法
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<K> {
// 返回双向链表结点
public final K next() { return nextNode() }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过LinkedHashMap的entrySet方法可以获取到LinkedEntrySet(LinkedHashMap类的成员内部类)对象,通过该对象可以根据数据删除LinkedHashMap中对应的双向链表结点,还可以获取LinkedEntryIterator(LinkedHashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取LinkedHashMap中每个双向链表结点

  • 使用迭代器获取LinkedHashMap中每个双向链表结点的key值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
* @author 奥数定理
* @version 1.0
*/
public class Map05 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new LinkedHashMap();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行LinkedHashMap的keySet方法,返回LinkedKeySet类对象(LinkedHashMap类的成员内部类),懒汉式单例设计模式
由于LinkedKeySet类继承了AbstractSet类,而AbstractSet类继承了AbstractCollection类并且实现了Set接口
故而对象引用的编译类型可以是Set接口
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
*/
Set set = map.keySet();
/*
(1) 对象引用set的运行类型为LinkedKeySet,故而执行LinkedKeySet类中iterator方法
final class LinkedKeySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedKeyIterator();
}
}
(2) 执行LinkedKeyIterator类(LinkedHashMap类的成员内部类)的无参构造方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode().getKey(); }
}
(3) 默认会执行LinkedKeyIterator类的父类LinkedHashIterator的无参构造方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

LinkedHashIterator() {
// 使下一个要返回的结点指针指向双向链表的头结点
next = head;
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 将当前要返回的结点指针置为null
current = null;
}
}
(4) 最后对象引用iterator指向LinkedKeyIterator对象
*/
Iterator iterator = set.iterator();
/*
(1)由于iterator对象引用则是指向LinkedKeyIterator对象(运行类型:LinkedKeyIterator),则是执行LinkedKeyIterator类的hasNext方法
但是LinkedKeyIterator类中没有hasNext方法,故而会去调用其父类LinkedHashIterator的hasNext方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 判断下一个要返回的结点是否为null,实际上是判断LinkedHashMap集合中的双向链表是否已经遍历完最后一个结点
public final boolean hasNext() {
return next != null;
}

final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
}
(2)由于iterator对象引用则是指向LinkedKeyIterator对象(运行类型:LinkedKeyIterator),则是执行LinkedKeyIterator类的next方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
(3)由于LinkedKeyIterator类继承了LinkedHashIterator类,LinkedKeyIterator类中又没有nextNode方法,故而执行其父类LinkedHashIterator的nextNode方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 该方法用于先让next和current指针后移,后返回current指向的结点
final LinkedHashMap.Entry<K,V> nextNode() {
// 记录next,防止后移next后找不到前一个结点
LinkedHashMap.Entry<K,V> e = next;
// 判断是否出现了并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 判断当前要返回的结点是否为null,即判断是否已经遍历完双向链表
if (e == null)
throw new NoSuchElementException();
// 使current后移
current = e;
// 使next后移
next = e.after;
// 返回current指向的结点
return e;
}
}

// Entry类(双向链表的结点)为LinkedHashMap类的静态内部类,继承了HashMap类的静态内部类Node(单链表的结点)
static class Entry<K,V> extends HashMap.Node<K,V> {
// 定义双向链表的前驱指针和后继指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
(4)执行Entry类(LinkedHashMap类的静态内部类)的getKey方法,但是Entry没有该方法,故而执行其父类Node(HashMap类的静态内部类)的getKey方法
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值
final K key; // key值
V value; // value值
Node<K,V> next; // 后继指针

// 返回单链表结点中记录的key值
public final K getKey() { return key; }
}
(5)继续执行LinkedKeyIterator类的next方法
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
// 返回双向链表结点中记录的key值
public final K next() { return nextNode().getKey(); }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过LinkedHashMap的keySet方法可以获取到LinkedKeySet(LinkedHashMap类的成员内部类)对象,通过该对象可以根据传入key值删除LinkedHashMap中对应的双向链表结点,还可以获取LinkedKeyIterator(LinkedHashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取LinkedHashMap中每个双向链表结点的key值

  • 使用迭代器获取LinkedHashMap中每个双向链表结点的value值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import java.util.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class Map06 {
@SuppressWarnings({"all"})
public static void main(String[] args) {
Map map = new LinkedHashMap();
map.put("no1", "陈洲");
map.put("no2", "奥数定理");
/*
(1) 执行LinkedHashMap的values方法,返回LinkedValues类对象(LinkedHashMap类的成员内部类)
由于LinkedValues类继承了AbstractCollection类,而AbstractCollection类继承了Collection接口
故而对象引用的编译类型可以是Collection接口
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new LinkedValues();
values = vs;
}
return vs;
}
*/
Collection collection = map.values();
/*
(1) 对象引用collection的运行类型为LinkedValues,故而执行LinkedValues类中iterator方法
final class LinkedValues extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedValueIterator();
}
}
(2) 执行LinkedValueIterator类(LinkedHashMap类的成员内部类)的无参构造方法
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode().getValue(); }
}
(3) 默认会执行LinkedValueIterator类的父类LinkedHashIterator的无参构造方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

LinkedHashIterator() {
// 使下一个要返回的结点指针指向双向链表的头结点
next = head;
// 记录当前集合修改数据的次数,防止在遍历过程中有其他线程修改了集合元素,导致数据不一致
expectedModCount = modCount;
// 将当前要返回的结点指针置为null
current = null;
}
}
(4) 最后对象引用iterator指向LinkedValueIterator对象
*/
Iterator iterator = collection.iterator();
/*
(1)由于iterator对象引用则是指向LinkedValueIterator对象(运行类型:LinkedValueIterator),则是执行LinkedValueIterator类的hasNext方法
但是LinkedValueIterator类中没有hasNext方法,故而会去调用其父类LinkedHashIterator的hasNext方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 判断下一个要返回的结点是否为null,实际上是判断LinkedHashMap集合中的双向链表是否已经遍历完最后一个结点
public final boolean hasNext() {
return next != null;
}
}
(2)由于iterator对象引用则是指向LinkedValueIterator对象(运行类型:LinkedValueIterator),则是执行LinkedValueIterator类的next方法
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getValue(); }
}
(3)由于LinkedValueIterator类继承了LinkedHashIterator类,LinkedValueIterator类中又没有nextNode方法,故而执行其父类LinkedHashIterator的nextNode方法
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next; // 下一个要返回的结点指针
LinkedHashMap.Entry<K,V> current; // 当前要返回的结点指针
int expectedModCount; //预期修改次数(用于并发检查)

// 该方法用于先让next和current指针后移,后返回current指向的结点
final LinkedHashMap.Entry<K,V> nextNode() {
// 记录next,防止后移next后找不到前一个结点
LinkedHashMap.Entry<K,V> e = next;
// 判断是否出现了并发修改
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 判断当前要返回的结点是否为null,即判断是否已经遍历完双向链表
if (e == null)
throw new NoSuchElementException();
// 使current后移
current = e;
// 使next后移
next = e.after;
// 返回current指向的结点
return e;
}
}

// Entry类(双向链表的结点)为LinkedHashMap类的静态内部类,继承了HashMap类的静态内部类Node(单链表的结点)
static class Entry<K,V> extends HashMap.Node<K,V> {
// 定义双向链表的前驱指针和后继指针
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
(4)执行Entry类(LinkedHashMap类的静态内部类)的getValue方法,但是Entry没有该方法,故而执行其父类Node(HashMap类的静态内部类)的getValue方法
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // hash值
final K key; // key值
V value; // value值
Node<K,V> next; // 后继指针

// 返回单链表结点中记录的value值
public final K getValue() { return value; }
}
(5)继续执行LinkedValueIterator类的next方法
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<K> {
// 返回双向链表结点中记录的value值
public final K next() { return nextNode().getValue(); }
}
*/
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println(next);
}
}
}

通过LinkedHashMap的values方法可以获取到LinkedValues(LinkedHashMap类的成员内部类)对象,通过该对象可以根据传入的value值删除LinkedHashMap中对应的双向链表结点,还可以获取LinkedValueIterator(LinkedHashMap类的成员内部类)对象(迭代器),通过该迭代器可以获取LinkedHashMap中每个双向链表结点的value值

Hashtable

  1. 基本介绍
  • 存放的元素是键值对:即K-V
  • 底层结构采用数组+链表来存储数据,没有红黑树
  • Hashtable的键和值都不能为null,否则会抛出NullPointerException
  • Hashtable 使用方法基本上和HashMap一样
  • Hashtable是线程安全的(synchronized),HashMap是线程不安全的
  1. 底层结构
  • 数组table的定义如下,默认是null,它是Hashtable的属性
1
private transient Entry<?,?>[] table;
  • 单链表类型定义如下,它是Hashtable的静态内部类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;

protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}

// Map.Entry Ops

public K getKey() {
return key;
}

public V getValue() {
return value;
}

public V setValue(V value) {
if (value == null)
throw new NullPointerException();

V oldValue = this.value;
this.value = value;
return oldValue;
}

public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;

return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}

public int hashCode() {
return hash ^ Objects.hashCode(value);
}

public String toString() {
return key.toString()+"="+value.toString();
}
}
  1. 添加数据的扩容机制
  • Hashtable的无参构造方法会调用其有参构造方法,将数组大小设置为11,设置加载因子为0.75,故而数组阈值为8
  • 添加数据时,会先通过hashCode方法获取到hash值,后将hash值与 0x7FFFFFFF(int类型的最大正整数值) 进行按位与运算(保证hash值为正数),最后将处理后的hash值 % 数组的长度得到索引值
  • 遍历索引值所在数组元素的单链表,如果有单链表结点中存储的key值与要添加的key值一致,则替换该结点中的value值,并返回替换前的value值
  • 否则将数据通过头插法添加到该链表的首部,但是在添加之前会先判断集合中存储的元素个数是否大于等于数组阈值,如果满足条件,会先对数组进行扩容处理,扩容后会重新计算索引值,故而先判断后添加
  • 扩容后的数组大小为原来的数组大小 * 2 + 1,阈值为扩容后的数组大小 * 0.75,将旧数组中数据拷贝到新数组的规则如下:
    • 依次遍历旧数组的数组元素,每次遍历过程中还要遍历该数组元素所在的单链表,通过该单链表中每个结点存储的hash值重新计算索引值,并将其通过头插法插入到新数组中索引值所在的单链表中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 遍历旧数组
for (int i = oldCapacity ; i-- > 0 ;) {
// 遍历数组中每个单链表
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
// 将当前遍历结点赋值给遍历e
Entry<K,V> e = old;
// 获取当前遍历结点的下一个结点,防止当结点添加到新数组时,丢失结点
old = old.next;

// 通过单链表结点存储的hash值重新计算索引值
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 让当前遍历结点的后继指针指向新数组中索引值所在数组元素
e.next = (Entry<K,V>)newMap[index];
// 将当前遍历结点插入到新数组中索引值所在单链表的表头
newMap[index] = e;
}
}

Properties

  1. 基本介绍
  • Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  • 键和值都不能为null,如果添加数据时存在相同的key,则会对value进行替换
  • Properties 还可以用于 从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
  1. 常用方法
  • get(Ojbect key):根据键获取值
  • load(Reader reader/ InputStream is):加载配置文件的键值对到Properties对象
  • lit(PrintStream printStream/ PrintWriter printWriter):将数据显示到指定设备
  • getProperty(Ojbect key):根据键获取值
  • setProperty(Object key, Object value):设置键值对到properties对象
  • store(Writer writer, String comments) 或者 store(OutputStream out, String comments):将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
  • remove(Objcet key):根据键删除键值对
  • put(Object key, Object value):如果key存在,则替换vlaue(改),如果key不存在,则将键值对添加到集合中(增)

TreeMap

  1. 基本介绍
  • TreeMap跟TreeSet底层原理一样,都是红黑树结构的。
  • 由键决定特性:不重复、无索引、可排序
  • 可排序:对键进行排序。

注意:默认按照键的从小到大进行排序,也可以自定义键的排序规则

  1. 自定义排序规则的方式有两种
  • 类实现Comparable接口,重写compareTo方法
  • 创建TreeMap对象时,通过构造器传递实现了Comparator接口并重写了compare方法的对象(可以通过匿名内部类进行传递)

Collections 工具类

常用方法(都是静态方法):

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

@SuppressWarnings({"all"})
public class CollectionsTest {
public static void main(String[] args) {
Collection collection = new ArrayList();
// 1.批量添加元素
Collections.addAll(collection, "java", "php", "vue", "spring");
System.out.println("批量添加后的集合元素为" + collection);
// 2.打乱元素顺序
ArrayList arrayList = (ArrayList) collection;
Collections.shuffle(arrayList);
System.out.println("打乱顺序后的集合元素为" + arrayList);
// 3.按照添加的数据的运行类型的默认排序顺序进行排序(该类型实现了Comparable接口,并重写compareTo方法)
Collections.sort(arrayList);
System.out.println("按照字母顺序进行排序后的集合元素为" + arrayList);
// 4.根据指定的规则进行排序
Collections.sort(arrayList, (o1, o2) -> ((String)o1).length() - ((String)o2).length());
System.out.println("按照字符串长度进行排序后的集合元素为" + arrayList);
// 5.按照二分查找法查找元素
int phpIndex = Collections.binarySearch(arrayList, "php");
System.out.println("根据二分查找法找到字符串php在集合中的位置为" + phpIndex);
// 6.拷贝集合中的元素
ArrayList newArrayList = new ArrayList(arrayList.size());
// 由于Collections.copy方法要求目标集合的长度必须大于等于源集合的长度,所以需要先创建一个长度为源集合长度的目标集合
for (int i = 0; i < arrayList.size(); i++) {
newArrayList.add(null);
}
Collections.copy(newArrayList, arrayList);
System.out.println("拷贝集合中的元素得到的新集合中的元素为" + newArrayList);
// 7.使用指定的元素填充集合
Collections.fill(newArrayList, "java");
System.out.println("用字符串java填充集合后的集合元素为" + newArrayList);
// 8.根据添加的数据的运行类型的默认排序顺序获取最大值/最小值
String minStr = (String) Collections.min(arrayList);
System.out.println("根据添加的数据的运行类型的默认排序顺序获取的最小值为" + minStr);
String maxStr = (String) Collections.max(arrayList);
System.out.println("根据添加的数据的运行类型的默认排序顺序获取的最大值为" + maxStr);
// 9.交换集合中指定位置的元素
Collections.swap(arrayList, 0 , 1);
System.out.println("交换集合中第一个元素和第二个元素" + arrayList);
}
}

Stream 流

  1. 基本介绍
  • Stream 流负责将集合或者数组中的数据转换为流,Stream 流提供了一系列的API,该API可以对流进行操作,最后可以得到处理完的数据
  • Stream 流提供了两类方法:中间方法和终结方法
  1. 将数据转换为流的方法如下
获取方式 方法名 说明
单列集合 default Stream stream() Collection中的默认方法
双列集合 无法直接使用stream流
数组 public static Stream stream(T[] array) Arrays工具类中的静态方法
一堆零散数据 public static Stream of(T … values) Stream接口中的静态方法

零散数据要求是同种类型的数据

Strem接口中的静态方法of的细节:方法的形参是一个可变参数,可以传递一堆零散数据,可以传递数组,但是数组必须保存的是引用数据类型的数据,如果保存的是基本数据类型,底层不会把数组中的每个元素进行自动装箱,而是会把整个数组作为一个元素放在stream流中

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

/**
* @author 奥数定理
* @version 1.0
*/
public class Test {
public static void main(String[] args) {
// 1.单列集合获取stream流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 静态内部类方式
// list.stream().forEach(new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// });
// lambda 表达式
list.stream().forEach(s -> System.out.println(s));

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

// 2.双列集合获取stream流
HashMap<String, Integer> hashMap = new LinkedHashMap<>();
hashMap.put("aaa", 111);
hashMap.put("bbb", 222);
hashMap.put("ccc", 333);
hashMap.put("ddd", 444);
// 第一种获取 stream 流的方式
hashMap.entrySet().stream().forEach(s -> System.out.println(s));
// 第二种获取 stream 流的方式
hashMap.keySet().stream().forEach(s -> System.out.println(s));
// 第三种获取 stream 流的方式
hashMap.values().stream().forEach(s -> System.out.println(s));

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

// 3.数组获取stream流
int[] arr = {1, 2, 3, 4, 5, 6};
Arrays.stream(arr).forEach(s -> System.out.println(s));
Stream.of(arr).forEach(s -> System.out.println(s));

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

// 4.零散数据获取stream流
Stream.of("1", "2", "3", "4", "5").forEach(s -> System.out.println(s));
}
}
  1. 中间方法
名称 说明
Stream filter(Predicate <? super T> predicate) 过滤
Stream limit(long maxSize) 获取前几个元素
Stream skip(long n) 跳过前几个元素
Stream distinct() 元素去重,依赖(hashCode和equals方法)
static Stream concat(Stream a, Stream b) 合并a和b两个流为一个流
Streammap(Function<T , R> mapper) 转换流中的数据类型
Optional reduce(BinaryOperator accumulator) 聚合流中的各个数据,聚合的含义就是将多个值经过特定计算之后得到单个值(Optional类中有get方法可以直接获取到计算后的单个值)

注意:

  1. 中间方法,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
  2. 修改Stream流中的数据,不会影响原来集合或者数组中的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
* @author 奥数定理
* @version 1.0
*/
public class Test02 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张三", "周芷若", "孙悟空", "张铁", "韩立");

// 1.filter 方法,过滤流中的数据
// list.stream().filter(new Predicate<String>() {
// @Override
// // test 方法中的形参表示流中的每一个数据
// public boolean test(String s) {
// // 当返回值为 true 时,表示保留该数据
// // 当返回值为 false 时,表示舍弃该数据
// return s.startsWith("张");
// }
// }).forEach(s -> System.out.println(s));

list.stream()
.filter(s -> s.startsWith("张"))
.forEach(s -> System.out.println(s));

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

// 2.limit 方法,获取前几个元素
list.stream()
.limit(3)
.forEach(s -> System.out.println(s));

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

// 3.skip 方法,跳过前几个元素
list.stream()
.skip(3)
.forEach(s -> System.out.println(s));

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

// 4.distinct 方法,流中元素去重,底层依靠HashSet类,即依靠hashCode和equals方法
list.add("张无忌");
list.add("张无忌");
list.add("张无忌");
list.add("张无忌");
list.stream()
.distinct()
.forEach(s -> System.out.println(s));

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

// 5.concat 方法,静态方法,合并两个流(建议两个流中数据类型保持一致,否则得到的流的类型为两个流中类型的共同的父类)
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, "李四", "王五");
Stream.concat(list.stream(), list1.stream()).forEach(s -> System.out.println(s));

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

// 6.map 方法,转换流中的数据
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "张无忌-12", "张三-45", "周芷若-58", "孙悟空-20", "张铁-23", "韩立-56");
// Function 接口中传递的两个泛型中,第一个表示流中元素原来的数据类型,第二个表示流中元素转换后的数据类型
// list2.stream().map(new Function<String, Integer>() {
// @Override
// // apply 方法中形参表示流中每个元素
// public Integer apply(String s) {
// String[] split = s.split("-");
// String ageString = split[1];
// int age = Integer.parseInt(ageString);
// return age;
// }
// }).forEach(s -> System.out.println(s));

list2.stream()
.map(s -> Integer.parseInt(s.split("-")[1]))
.forEach(s -> System.out.println(s));

List<Integer> list3 = Lists.newArrayList(1,2,3,4,5);
// 将数组进行累加求和
// 由于返回的是 Optional ,因此需要get()取出值
// 第一个参数 result :初始值为集合中的第一个元素,后面为每次的累加计算结果 ;
// 第二个参数 item :遍历的集合中的每一个元素(从第二个元素开始,第一个被result使用了)
Integer total = list3.stream().reduce((result,item)->result+item).get();
System.out.println(total);
}
}
  1. 终结方法
名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.Consumer;
import java.util.function.IntFunction;

/**
* @author 奥数定理
* @version 1.0
*/
public class Test03 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张三", "周芷若", "孙悟空", "张铁", "韩立");

// 1.forEach 方法,遍历流中元素
// Consumer 接口的泛型表示流中数据的数据类型
// list.stream().forEach(new Consumer<String>() {
// @Override
// // accept 方法的形参为流中每一个数据
// // accept 方法则是负责遍历流中每一个数据
// public void accept(String s) {
// System.out.println(s);
// }
// });
list.stream()
.forEach(s -> System.out.println(s));

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

// 2.count 方法,统计流中元素个数
long count = list.stream().count();
System.out.println("list中元素个数为" + count);

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

// 3.toArray 方法,将流中元素放入数组中
// 第一种使用方式
Object[] array = list.stream().toArray();
System.out.println(Arrays.toString(array));

// 第二种使用方式
// IntFunction 接口的泛型表示返回的数组的类型
// toArray 方法的参数的作用是负责创建一个指定类型的数组
// toArray 方法的底层负责将流中每个元素放入数组中
// toArray 方法的返回值为装有流中所有数据的数组
// String[] strs = list.stream().toArray(new IntFunction<String[]>() {
// @Override
// // apply 方法的形参表示流中数据的个数,一般要和数组的长度保持一致
// // apply 方法的返回值为具体类型的数组
// public String[] apply(int value) {
// return new String[value];
// }
// });

String[] strs = list.stream().toArray(value -> new String[value]);
System.out.println(Arrays.toString(strs));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* @author 奥数定理
* @version 1.0
*/
public class Test04 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-12", "张三-女-45", "周芷若-女-58", "孙悟空-男-20", "张铁-男-23", "韩立-男-56", "李四-女-28", "王五-男-29");
// 需求:收集所有男性数据
// 4.collect 方法,收集流中的数据,放到集合中
// (1)将流中数据放入List集合中
List<String> list1 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
// Collectors.toList() 方法底层会创建一个 ArrayList 类的对象
// collect 方法会将流中的数据放入集合中
.collect(Collectors.toList());
System.out.println(list1);

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

// (2)将流中数据放入Set集合中,该方式会将流中重复元素给去除
Set<String> set = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
// Collectors.toSet() 方法底层会创建一个 HashSet 类的对象
// collect 方法会将流中的数据放入集合中
.collect(Collectors.toSet());
System.out.println(set);

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

// 需求:将姓名作为键,年龄作为值
// (3)将流中数据放入Map集合中
/*
* Collectors.toMap 方法底层负责创建一个指定数据类型的HashMap集合
* Collectors.toMap 方法的参数分别为生成键的规则,生成值的规则
* 参数一:
* Function 接口的泛型一:流中数据的数据类型
* 泛型二:生成的键的数据类型
* apply 方法的作用是生成键
* 返回值:返回键
* 形参:流中每一个数据
* 参数二:
* Function 接口的泛型一:流中数据的数据类型
* 泛型二:生成的值的数据类型
* apply 方法的作用是生成值
* 返回值:返回值
* 形参:流中每一个数据
*/
// Map<String, Integer> map = list.stream()
// .filter(s -> "男".equals(s.split("-")[1]))
// .collect(Collectors.toMap(new Function<String, String>() {
// @Override
// public String apply(String s) {
// return s.split("-")[0];
// }
// }, new Function<String, Integer>() {
// @Override
// public Integer apply(String s) {
// return Integer.parseInt(s.split("-")[2]);
// }
// }));

Map<String, Integer> map = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toMap(s -> s.split("-")[0], s -> Integer.parseInt(s.split("-")[2])));
System.out.println(map);
}
}

泛型

  1. 基本介绍
  • 泛型又称为参数化类型,是JDK1.5出现的新特性,解决数据类型的安全性问题
  • 在类声明或示例化时只要指定好需要的具体类型
  • 泛型的作用:可以在声明类时通过一个标识标识类中某个属性的类型,或者是某个方法的返回值的类型,或者是参数类型
  1. 泛型的使用
  • 泛型的声明
1
2
interface 接口名<T> {}
class 类名<K, V> {}
  • 泛型的实例化
1
2
3
类名<数据类型> 对象名 = new 类名<数据类型>();
或者
类名<数据类型> 对象名 = new 类名<>();
  1. 使用细节
  • 给泛型指定数据类型时,要求时引用类型,不能是基本数据类型
  • 给泛型指定具体类型后,可以传入该类型或者其子类型
  • 如果不给泛型指定具体类型,则泛型默认是Object

自定义泛型类

  1. 基本语法
1
2
3
class 类名<T, R, ...> { // 可以有多个泛型
// 类体
}
  1. 使用细节
  • 普通成员可以使用泛型(方法、属性)
  • 使用泛型的数组,不能初始化(因为不确定类型,就无法在JVM内存中开辟空间)
  • 静态成员中不能使用类的泛型,因为静态方法和属性在类加载时就会加载,而创建对象是在类加载之后,所以如果静态方法或者属性使用了泛型,JVM无法完成初始化
  • 泛型类中的泛型的类型是在创建对象时确定的(因为创建对象时,需要指定确定类型)
  • 如果在创建对象时,没有指定类型,则默认为Object类型
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
public class Generic01 {
public static void main(String[] args) {
Person<String, Integer> person = new Person<>("zhangsan", 18);
System.out.println(person.getR());
System.out.println(person.getT());
person.setR(20);
person.setT("lisi");
System.out.println(person);
}
}
class Person<T, R> {
T t;
R r;

public Person(T t, R r) {
this.t = t;
this.r = r;
}

public T getT() {
return t;
}

public void setT(T t) {
this.t = t;
}

public R getR() {
return r;
}

public void setR(R r) {
this.r = r;
}

@Override
public String toString() {
return "Person{" +
"t=" + t +
", r=" + r +
'}';
}
}

自定义泛型接口

  1. 基本语法
1
2
3
interface 接口名<T, R, ...> {
// 接口体
}
  1. 使用细节
  • 接口中,静态成员也不能使用泛型,原因和泛型类一致
  • 泛型接口的类型,在继承接口或者实现接口时确定
  • 没有指定类型,默认为Object类型
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
public class Generic02 {
public static void main(String[] args) {

}
}

// 使用接口继承泛型接口时,给泛型指定数据类型
interface IA extends IUsb<String, Double> {

}

// 由于接口IA在继承接口IUsb时,已经给泛型制定了数据类型,故而实现IA的类不能指定类型
class A implements IA {

@Override
public String run(String s, Double aDouble) {
return "";
}

@Override
public void print(String t1, String t2, Double r1, Double r2) {

}
}

// 使用类实现泛型接口时,给泛型指定数据类型
class B implements IUsb<Float, Double> {

@Override
public Float run(Float aFloat, Double aDouble) {
return 0f;
}

@Override
public void print(Float t1, Float t2, Double r1, Double r2) {

}
}

// 自定义泛型接口
interface IUsb<T, R> {
int n = 10;
// 因为接口中的属性默认为静态属性,故而不可以使用泛型定义接口属性
// T t;

T run(T t, R r);

void print(T t1, T t2, R r1, R r2);
}

自定义泛型方法

  1. 基本语法:
1
2
3
访问修饰符 <T, R, ...> 返回类型 方法名(形参列表) {
// 方法体
}
  1. 使用细节:
  • 泛型方法可以定义在普通类中,也可以定义在泛型类中
  • 当泛型方法被调用时,类型就会被确认
  • public void eat(E e) {},修饰符后没有<T, R, …>,eat方法不是泛型方法,而是eat方法使用了泛型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Generic03 {
public static void main(String[] args) {
Car<String, Double> car = new Car<>();
car.run("宝马", 18.2);
System.out.println("========");
// 泛型方法中定义的泛型在调用方法时确认数据类型
car.fly("宝马", "A7", 28.2);
System.out.println("========");
car.fly(28, "宝马", 28.2);
System.out.println("========");
Dog dog = new Dog();
dog.run("你好", new ArrayList<>());
}
}
// 泛型方法可以定义在泛型类中
class Car<T, R> {
// 普通方法使用了泛型
public void run(T t, R r) {
System.out.println(t.getClass());
System.out.println(r.getClass());
}

// 泛型方法使用了类定义的泛型,也使用了自己定义的泛型
public <E> T fly(E e, T t, R r) {
System.out.println(e.getClass());
System.out.println(t.getClass());
System.out.println(r.getClass());
return t;
}
}
// 泛型方法可以定义在普通类中
class Dog {
public <T, R> void run(T t, R r) {
System.out.println(t.getClass());
System.out.println(r.getClass());
}
}

泛型的继承和通配符

基本介绍

  • :支持任意泛型类型
  • :支持A类以及A类的子类,规定了泛型的上限
  • :支持A类以及A类的父类,但并不限于直接父类,规定了泛型的下限
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
package com.itcz.generic;

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

/**
* @author 奥数定理
* @version 1.0
*/
public class Generic04 {
public static void main(String[] args) {
List<Object> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>();
List<AA> list3 = new ArrayList<>();
List<BB> list4 = new ArrayList<>();
List<CC> list5 = new ArrayList<>();

collection1(list1);
collection1(list2);
collection1(list3);
collection1(list4);
collection1(list5);

// Object 不是AA类的子类
// collection2(list1);
// String 也不是AA类的子类
// collection2(list2);
collection2(list3);
collection2(list4);
collection2(list5);

collection3(list1);
// String 不是AA类的父类
// collection3(list2);
collection3(list3);
// BB 不是 AA 类的父类
// collection3(list4);
// CC 不是 AA 类的父类
// collection3(list5);
}

// 可以传递任意泛型
public static void collection1(List<?> list) {

}

// 可以传递AA类或者AA类的子类
public static void collection2(List<? extends AA> list) {

}

// 可以传递AA类或者AA类的父类,但是不限于直接父类
public static void collection3(List<? super AA> list) {

}
}
class AA {

}
class BB extends AA {

}
class CC extends BB {

}

Optional 类

  1. 基本概念:Optional 类(java.util.Optional)是一个容器类,它可以保存类型T的值,代表这个值存在,或者仅仅保存null,表示这个值不存在。原来用null表示一个值不存在,现在 Optional 可以更好的表达这个概念,并且可以有效避免空指针异常
  2. 常用方法
方法名 说明
public static of(T t) 创建一个 Optional 实例,t必须非空
public static empty() 创建一个空的 Optional 实例
public static ofNullable(T t) 创建一个 Optional 实例,t可以为null
T orElse(T other) 如果有值则将其返回,否则返回指定的other对象
T get() 如果调用对象包含值,则返回该值,否则会报空指针异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.Optional;

/**
* @author 奥数定理
* @version 1.0
*/
public class Test01 {
public static void main(String[] args) {
Dog dog = new Dog();
dog = null;
Optional<Dog> optionalDog = Optional.ofNullable(dog);
System.out.println(optionalDog);

Dog tom = optionalDog.orElse(new Dog("tom", 18));
System.out.println(tom);
}
}

class Dog {
private String name;
private int age;

public Dog() {
}

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

方法引用

  1. 基本介绍:把已经存在的方法的方法体作为函数式接口中的抽象方法的方法体
  2. 要求
  • 引用处必须是函数式接口
  • 被引用的方法必须已经存在
  • 被引用的方法的形参形式和返回值形式必须和抽象方法保持一致
  • 被引用方法的功能要满足当前需求

引用静态方法

格式:类名::方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;

/**
* @author 奥数定理
* @version 1.0
*/
public class StaticMethod {
public static void main(String[] args) {
// 需求:将集合中的所有字符串数据转成整型
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "1", "2", "3", "4", "5");
// 1.静态内部类
// list.stream().map(new Function<String, Integer>() {
// @Override
// public Integer apply(String s) {
// return Integer.parseInt(s);
// }
// }).forEach(s -> System.out.println(s));

// 2.方法引用
list.stream()
.map(Integer::parseInt)
.forEach(s -> System.out.println(s));

}
}

引用成员方法

格式

  • 其他类:其他类对象::方法名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo02 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张三", "周芷若", "孙悟空", "张铁", "韩立");

// 需求:获取名字以张开头,并且长度为3
// list.stream()
// .filter(new Predicate<String>() {
// @Override
// public boolean test(String s) {
// return s.startsWith("张") && s.length() == 3;
// }
// }).forEach(s -> System.out.println(s));

list.stream()
.filter(new StringOperation()::stringJudge)
.forEach(s -> System.out.println(s));
}
}

class StringOperation {
public boolean stringJudge(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
  • 本类:this::方法名

引用处不能是静态方法的内部,因为静态方法内部不能使用this关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Predicate;

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo03 {
public static void main(String[] args) {
new FunctionDemo03().method();
}

public void method() {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张三", "周芷若", "孙悟空", "张铁", "韩立");

// 需求:获取名字以张开头,并且长度为3
// list.stream()
// .filter(new Predicate<String>() {
// @Override
// public boolean test(String s) {
// return s.startsWith("张") && s.length() == 3;
// }
// }).forEach(s -> System.out.println(s));

list.stream()
.filter(this::stringJudge)
.forEach(s -> System.out.println(s));
}

public boolean stringJudge(String s) {
return s.startsWith("张") && s.length() == 3;
}
}
  • 父类:super::方法名

引用处不能是静态方法的内部,因为静态方法内部不能使用super关键字

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

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo04 extends FatherClass{
public static void main(String[] args) {
new FunctionDemo04().method();
}

public void method() {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张三", "周芷若", "孙悟空", "张铁", "韩立");

// 需求:获取名字以张开头,并且长度为3
// list.stream()
// .filter(new Predicate<String>() {
// @Override
// public boolean test(String s) {
// return s.startsWith("张") && s.length() == 3;
// }
// }).forEach(s -> System.out.println(s));

list.stream()
.filter(super::stringJudge)
.forEach(s -> System.out.println(s));
}
}

class FatherClass {
public boolean stringJudge(String s) {
return s.startsWith("张") && s.length() == 3;
}
}

引用构造方法

格式:类名::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
67
68
69
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo05 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张三-18", "李四-20", "王五-21", "赵六-45", "孙悟空-456");
// List<Student> newList = list.stream().map(new Function<String, Student>() {
// @Override
// public Student apply(String s) {
// String[] split = s.split("-");
// String name = split[0];
// int age = Integer.parseInt(split[1]);
// return new Student(name, age);
// }
// }).collect(Collectors.toList());

// 引用构造方法
List<Student> newList = list.stream().map(Student::new).collect(Collectors.toList());
System.out.println(newList);
}
}

class Student {
private String name;
private int age;

public Student(String str) {
String[] split = str.split("-");
this.name = split[0];
this.age = Integer.parseInt(split[1]);
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

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

通过类名::方法名引用该类中的方法

  1. 注意:通过类名::方法名引用该类中的方法时,该方法可以是静态方法,也可以是非静态方法
  2. 要求
  • 被引用的地方需要是函数式接口
  • 被引用的方法必须已经存在
  • 被引用方法的形参,需要跟抽象方法的第二个形参到最后一个形参保持一致,返回值类型需要保持一致
  • 被引用方法的功能需要满足当前需求
  • 函数式接口中的抽象方法形参详解
    • 第一个参数:表示被引用方法的调用者,决定了可以引用哪些类中的方法,在stream流中,第一个参数一般表示流里面的每一个数据。例如流里面的数据的数据类型是String类型,那么使用这种方式进行方法引用时,只能引用String类中的方法
    • 第二个参数到最后一个参数必须和被引用的形参列表保持一致,如果抽象方法中没有第二个参数,说明被引用的方法需要是无参的成员方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.util.ArrayList;
import java.util.Collections;
import java.util.function.Function;

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo06 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "aaa", "bbb", "ccc", "ddd");
// 需求:将集合中的数据全部变成大写后输出
// 使用匿名内部类的方式
list.stream().map(new Function<String, String>() {
@Override
public String apply(String s) {
return s.toUpperCase();
}
}).forEach(s -> System.out.println(s));

// 使用方法引用的方式
/*
被引用的方法的形式如下:
public String toUpperCase() {
return toUpperCase(Locale.getDefault());
}
*/
list.stream().map(String::toUpperCase).forEach(s -> System.out.println(s));
}
}

引用数组的构造方法

格式:数据类型[]::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
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.function.IntFunction;

/**
* @author 奥数定理
* @version 1.0
*/
public class FunctionDemo07 {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5);
// 需求:将集合中的数据收集到数组中
// 匿名内部类方式
// list.stream().toArray(new IntFunction<Integer[]>() {
// @Override
// public Integer[] apply(int value) {
// return new Integer[value];
// }
// });

// 方法引用方式
Integer[] array = list.stream().toArray(Integer[]::new);
System.out.println(Arrays.toString(array));
}
}

多线程基础

进程

  1. 基本介绍
  • 进程是指运行中的程序,例如使用QQ程序,就操作系统会在计算机中创建一个进程,操作系统会为该进程分配内存空间
  • 进程是程序的一次执行过程,或是正在运行的一个程序,是动态过程:有它本身的产生、存在和消亡的过程
  1. 并发:同一时刻,多个任务交替执行,因为切换频率快,造成了“貌似同时”的错觉,单核CPU实现的多任务就是并发
  2. 并行:同一时刻,多个任务同时执行,多核CPU可以实现并行

线程

  1. 基本介绍
  • 线程可以由线程或者进程创建,它是进程的一个实体
  • 一个进程可以拥有多个线程
  • 单线程:同一时刻,只允许执行一个线程
  • 多线程:同一时刻,可以执行多个线程,比如:一个QQ进程,可以同时打开多个聊天窗口,一个迅雷进程,可以同时下载多个文件
  1. 通过继承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
44
45
46
47
48
public class Thread01 {
public static void main(String[] args) {
Cat cat = new Cat();
// 启动线程
/*
(1) 执行start方法(由于cat对象引用的运行类型是Cat类,但是Cat类中没有start方法,故而执行其父类Thread的start方法)
public synchronized void start() {
start0();
}
(2) 执行start0方法(本地方法),由JVM机调用,更底层则是由C/C++来实现多线程
当start0方法被JVM机调用后,会执行cat对象引用的run方法
private native void start0();
(3) 执行cat对象引用的run方法(由于继承Thread类,并重写run方法,故而会执行Cat类的run方法)
*/
cat.start();
// 如果主线程执行的是下列这条语句,那么run方法就不是在子线程
// 中执行的,而是在主线程中执行的,因此主线程被阻塞,直到run
// 方法执行完毕后才会执行后面的代码
// cat.run();
for (int i = 0; i < 6; i++) {
System.out.println(Thread.currentThread().getName() + "线程正在执行" + (i + 1));
try {
// 让当前线程休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

// 继承Thread类
class Cat extends Thread {
// 重写run方法,编写业务逻辑
@Override
public void run() {
int times = 0;
do {
System.out.println(Thread.currentThread().getName() + "线程正在打印:喵喵,我是一只小猫咪" + (++times));
try {
// 让当前线程休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} while (times <= 8);
}
}

该程序在JVM底层执行过程:当Thread01类执行后,会在JVM内存中创建一个进程,然后执行main方法时,该进程会创建一个主线程,主线程执行到Cat cat = new Cat();语句时,主线程会创建一个子线程,主线程执行到cat.start();语句时,主线程会启动子线程,此时子线程会开始执行Cat类中的run方法,执行里面的逻辑,并且主线程不会被阻塞,在执行过程中主线程会被销毁(因为main方法执行完毕),但是此时进程不会被销毁,因为子线程还在执行,直到子线程执行完所有的业务逻辑后,进程才会被销毁

  1. 通过实现Runable接口创建线程类

由于Java只支持单继承,故而当某个类继承了其父类时,就无法通过继承Thread类来创建线程,故而引出了Runable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.itcz.thread;

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread02 {
public static void main(String[] args) {
Dog tom = new Dog("tom", 18);
// 由于只能通过Thread类中start方法中的start0方法才能实现多线程
// 而Runnable接口中没有start方法,故而需要使用静态代理模式来实现启动线程(主要是要调用Thread类中的start0方法)
/*
(1) 执行Thread类的有参构造方法,会将实现了Runnable接口的Dog类对象引用tom传递给Thread类中的属性target(private Runnable target;)
(2) 执行start方法,start方法会执行start0方法
(3) 当start0方法被JVM机调用后,会执行thread对象引用的run方法(由于没有继承Thread类,并重写run方法,故而还是执行Thread类的run方法)
(4) 执行Thread类的run方法,当target不为null时,执行target对象引用的run方法
@Override
public void run() {
if (target != null) {
target.run();
}
}
(5) 执行target对象引用的run方法(由于target的运行类型是Dog,故而执行Dog类中的run方法)
*/
Thread thread = new Thread(tom);
thread.start();
}
}
class Dog extends Animal implements Runnable {

public Dog(String name, double age) {
super(name, age);
}

@Override
public void run() {
int times = 0;
do {
System.out.println("小狗" + this.getName() + "嗷嗷叫~~~" + (++times) + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} while (times != 10);
}
}

class Animal {
private String name;
private double age;

public String getName() {
return name;
}

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

public double getAge() {
return age;
}

public void setAge(double age) {
this.age = age;
}

public Animal(String name, double age) {
this.name = name;
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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
package com.itcz.thread;

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread03 {
public static void main(String[] args) {
T1 t1 = new T1();
t1.start();

T2 t2 = new T2();
ThreadProxy threadProxy = new ThreadProxy(t2);
threadProxy.start();
}
}
class ThreadProxy {
private Runnable target;

public ThreadProxy(Runnable target) {
this.target = target;
}

public ThreadProxy() {
}

public void run() {
if (this.target != null) {
target.run();
}
}

public void start() {
start0();
}

public void start0() {
// C/C++实现多线程
this.run();
}
}

class T1 extends ThreadProxy {
@Override
public void run() {
System.out.println("子线程执行T1类中的run方法");
}
}

class T2 implements Runnable {
@Override
public void run() {
System.out.println("子线程执行T2类中的run方法");
}
}
  1. 继承Thread类 和 实现Runnable接口的区别
  • 继承Thread类实现多线程,不便于多个线程共享同一资源
  • 而实现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
package com.itcz.thread;

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread04 {
public static void main(String[] args) {
T3 t3 = new T3();
Thread thread1 = new Thread(t3);
Thread thread2 = new Thread(t3);
thread1.start();
thread2.start();
}
}
class T3 implements Runnable {
@Override
public void run() {
while(true) {
int times = 0;
do {
System.out.println(Thread.currentThread().getName() + "线程在执行" + (++times));
try {
// 让当前线程休眠一秒
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} while (times <= 8);
}
}
}
  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
public class Thread06 {
public static void main(String[] args) {
T t = new T();
t.start();

System.out.println("主线程休眠十秒");

try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

// 关闭子线程
t.setLoop(false);
}
}

class T extends Thread{
private boolean loop = true;
@Override
public void run() {
while (loop) {
System.out.println("T线程正在运行.....");

try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

public void setLoop(boolean loop) {
this.loop = loop;
}
}
  1. 线程的生命周期
  • new 状态:创建态,当该线程被创建好,但是未获取到它需要的资源时所处于的状态
  • Runnable 状态:可运行状态,又可被细分为就绪态和运行态,就绪态是指线程已获取到除CPU以外的资源所处于的状态,运行态则是指线程已获取到CPU,并且正在运行的状态
  • TimedWaiting 超时等待状态:当线程调用sleep(int time)方法、wait(int time)、LockSupport.parkNanos()、LockSupport.parkUntil()或者当其他线程加入时(其他线程类对象调用join(int time)方法),线程会处于超时等待状态。当时间结束后会进入可运行状态
  • Waiting 等待状态:当线程调用wait(),LockSupport.park()或者当其他线程加入时(其他线程类对象调用join()方法),线程会处于等待状态。当线程调用notify()、notifyAll()或者LockSupport.unpark()方法时,会进入可运行状态
  • Blocked 阻塞状态:当线程等待获取进入同步代码块的锁时,会进入阻塞态,获取到锁后会进入可运行状态
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
public class Thread09 {
public static void main(String[] args) {
T6 t6 = new T6();
// 输出线程所处的状态
System.out.println("线程所处的状态:" + t6.getState());
// 启动线程
t6.start();
System.out.println("线程所处的状态:" + t6.getState());

for(int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程所处的状态:" + t6.getState());
}

System.out.println("线程所处的状态:" + t6.getState());
}
}

class T6 extends Thread{
@Override
public void run() {
for(int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("h1 " + i);
}
}
}

Thread 线程类

  1. 基本介绍
  • Thread 类实现了 Runnable 接口,重写了其中的 run 方法
  • Thread 类中有一个 Runnable 接口类型的 target 属性,用来接收实现 Runnable 接口的类的对象引用,进而通过静态代理方式实现多线程
  1. 常用方法
  • setName(String name):设置线程名
  • getName():返回该线程的名称
  • start():使该线程开始执行,底层调用start0方法
  • run():调用线程对象的 run 方法
  • setPriority(int priority):更改线程优先级,优先级范围从1到10
  • getPriority():获取线程优先级
  • sleep(int time):让线程休眠指定毫秒数,静态方法
  • interrupt():中断线程,中断线程不是终止线程,只是让线程执行到该语句时,报一个异常
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
public class Thread07 {
public static void main(String[] args) {
T4 t4 = new T4();
t4.setName("tom");
t4.start();

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

for (int i = 0; i < 5; i++) {
System.out.println("主线程正在运行" + i);
}

// 中断线程,让线程退出休眠
t4.interrupt();
}
}
class T4 extends Thread{
@Override
public void run() {
while (true) {
for(int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "线程正在运行" + i);
}

// 休眠20秒
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
// 一旦在线程休眠时间内出现了中断异常,则会被catch到,线程会立刻退出休眠
System.out.println(Thread.currentThread().getName() + "线程被中断,退出休眠");
}
}
}
}
  • yield():线程的礼让。让出cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功,静态方法
  • join():线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Thread08 {
public static void main(String[] args) {
T5 t5 = new T5();
t5.start();
// try {
System.out.println("主线程让子线程加入,主线程等子线程执行完毕后再执行");
// 加入线程
// t5.join();
// 主线程礼让CPU,当CPU资源足够的话,并不会成功
Thread.yield();
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
System.out.println("主线程继续执行");
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程正在运行...." + (i));
}
}
}

class T5 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("子线程正在运行...." + (i));
}
}
}
  • setDeamon(boolean flag):将该线程设置为守护线程

用户线程:也称为工作线程,一般都是任务执行完毕或者以通知方式终止

守护线程:一般是为工作线程服务,当所有的用户线程结束后,守护线程自动结束,比如:垃圾回收机制

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

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread09 {
public static void main(String[] args) {
T6 t6 = new T6();
// 将子线程设置为守护线程
t6.setDaemon(true);
// 启动线程
t6.start();

for(int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程正在运行.....");
}
}
}

class T6 extends Thread{
@Override
public void run() {
for(;;) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("守护线程正在运行.....");
}
}
}

线程同步机制

  1. 基本介绍
  • 在多线程中,一些敏感数据不允许被多个线程同时访问,此时就需要使用同步访问技术,以保证数据在任何时候,最多只有一个线程访问,以保证数据的完整性
  • 当一个线程对某个内存地址进行操作时,其他线程都不允许对其进行操作,都会进入堵塞队列
  1. 实现同步的方式
  • 可以在代码块上添加 synchronized 关键字
1
2
3
synchronized (对象) {
// 代码块
}
  • 可以在方法上添加 synchronized 关键字,让该方法变成同步方法
1
2
3
访问修饰符 synchronized 返回类型 方法名(形参列表) {
// 方法体
}
  1. 互斥锁
  • Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。
  • 每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
  • 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
  • 同步的局限性:导致程序的执行效率要降低
  • 同步方法(非静态的)的锁是加在this上,同步方法(静态的)的锁为当前类.class对象
  • 非静态方法中的同步代码块的互斥锁可以加在this对象上,也可以是加在其他对象(要求多个线程必须操作的是同一个对象的同步方法)
  • 静态方法中的同步代码块的互斥锁只能加在当前类.class对象
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
public class Thread05 {
public static void main(String[] args) {
SellTicket01 sellTicket01 = new SellTicket01();

// 启动线程,五个线程都是操作的同一个对象(要求多个线程必须操作的是同一个对象的同步方法)
new Thread(sellTicket01).start(); // 线程1
new Thread(sellTicket01).start(); // 线程2
new Thread(sellTicket01).start(); // 线程3
new Thread(sellTicket01).start(); // 线程4
new Thread(sellTicket01).start(); // 线程5
}
}

class SellTicket01 implements Runnable {
private static int ticketNum = 100;
private boolean loop = true;
private final Object object = new Object();
@Override
public void run() {
while (this.loop) {
sell();
}
}

// 非静态同步方法,并且该互斥锁是加在this对象上的
public /* synchronized */ void sell() {
// 非静态方法中的同步代码块的互斥锁可以加在this对象上,也可以是加在其他对象(比如SellTicket01类的object属性)
synchronized (/* this */ object) {
if (ticketNum > 0) {
System.out.println(Thread.currentThread().getName() + "线程售出一张票,剩余票数为" + (--ticketNum));
}

try {
Thread.sleep(50);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

if (ticketNum <= 0) {
System.out.println(Thread.currentThread().getName() + "线程结束");
this.loop = false;
}
}
}

// 非静态同步方法,并且该互斥锁是加在SellTicket01类上的
public /* synchronized */ static void sell1() {
// 静态方法中的同步代码块的互斥锁只能加在类上
synchronized (SellTicket01.class) {

}
}
}
  1. 死锁:多个线程都占用了对方的锁资源,但是不肯相让,这就形成了死锁状态

线程池

  1. 基本介绍
  • 当继承Thread类和实现Runnable接口的线程类的任务执行完毕后,该线程就会被销毁,当再次需要该线程时,又会重新去启动该线程,这会导致资源的浪费,启动线程也需要大量的时间
  • 当某个任务需要线程进行处理时,线程池会分配给该任务一个线程,当任务结束后不会销毁该线程,有利于节约资源,每次分配时,会先判断线程池中是否有空闲线程,如果没有,会再次创建新的线程
  1. 可以通过Executors类来获取线程池对象
方法名称 说明
public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池
public static ExecutorService newFixedThreadPool(int nThreads) 创建有上限的线程池
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread10 {
public static void main(String[] args) {
// 1.获取线程池对象
ExecutorService pool1 = Executors.newCachedThreadPool();
ExecutorService pool2 = Executors.newFixedThreadPool(3);

// 2.提交任务
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool1.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());
pool2.submit(new MyRunnable());

// 3.销毁线程池
pool1.shutdown();
pool2.shutdown();
}
}
class MyRunnable implements Runnable {

@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("pool----" + Thread.currentThread().getName() + "-------" + i);
}
}
}

自定义线程池

  1. 可以通过ThreadPoolExecutor类的构造方法创建自定义线程池对象
  2. 自定义线程池处理任务的机制:当任务需要线程时,会先给任务分配核心线程,当核心线程数量达到上限后,会让后续任务阻塞在阻塞队列中,当阻塞队列中的任务达到队列中的任务上限时,会创建临时线程处理后续任务(这些任务不是阻塞队列里面的,而是此时超过阻塞队列上限时,后续需要处理的任务),当核心线程和临时线程的总数达到线程池的最大线程数时,会采取任务拒绝策略处理后续任务
  3. 形参列表
  • 参数一:核心线程的数量(不能小于0)
  • 参数二:线程池最大线程的数量(最大数量>=核心线程数量)
  • 参数三:空闲时间(值)(不能小于0)
  • 参数四:空闲时间(单位)(用TimeUnit枚举类指定)

空闲时间表示当临时线程等待该时间长度,都没有分配给任务时,会销毁该临时线程

  • 参数五:阻塞队列(不能为null)
    • ArrayBlockingQueue:有界的阻塞队列
    • LinkedBlockingQueue:无界的阻塞队列,但不是真正的无界,最大为int的最大值
  • 参数六:创建线程的方式(不能为null)
    • 一般传入Executors.defaultThreadFactory()
  • 参数七:要执行的任务过多时的解决方案(不能为null)
  1. 任务拒绝策略(静态内部类)
任务拒绝策略 说明
ThreadPoolExecutor. AbortPolicy 默认策略:丢弃任务并抛出RejectedExecutionException异常
ThreadPoolExecutor. DiscardPolicy 丢弃任务,但是不抛出异常这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* @author 奥数定理
* @version 1.0
*/
public class Thread11 {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
3, // 核心线程数量
6, // 最大线程数量
60, // 空闲时间(值)
TimeUnit.SECONDS, // 空闲时间(单位)
new ArrayBlockingQueue<>(3), // 阻塞队列
Executors.defaultThreadFactory(), // 创建线程的方式
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

threadPoolExecutor.submit(new MyRunnable());
threadPoolExecutor.submit(new MyRunnable());
threadPoolExecutor.submit(new MyRunnable());
threadPoolExecutor.submit(new MyRunnable());

threadPoolExecutor.shutdown();
}
}

class MyRunnable implements Runnable {

@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println("pool----" + Thread.currentThread().getName() + "-------" + i);
}
}
}

IO 流

文件

  1. 概念:文件就是保存数据的地方
  2. 文件流:数据在数据源(文件)和程序(内存)之间经历的路径
  • 输入流:数据从数据源(文件)到程序(内存)的路径
  • 输出流:数据从程序(内存)到数据源(文件)的路径
  1. 创建文件对象相关构造器和方法
  • new File(String pathname) // 根据路径构建一个File对象
  • new File(File parent,String child) // 根据父目录文件+子路径构建
  • new File(String parent,String child) // 根据父目录+子路径构建
  • createNewFile 创建新文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.io.File;
import java.io.IOException;

public class FileCreate {
public static void main(String[] args) {
// 方式一
File file1 = new File("E:\\newFile1.txt");
try {
file1.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}

// 方式二
File parentFile = new File("E:\\");
File file2 = new File(parentFile, "newFile2.txt");
try {
file2.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}

// 方式三
String parentPath = "E:\\";
String fileName = "newFile3.txt";
File file3 = new File(parentFile, fileName);
try {
file3.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
  1. 常用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.io.File;
import java.io.IOException;

public class FileInformation {
public static void main(String[] args) {
File file1 = new File("E:\\newFile1.txt");
try {
file1.createNewFile();
} catch (IOException e) {
throw new RuntimeException(e);
}

System.out.println("文件名字=" + file1.getName());
System.out.println("文件绝对路径=" + file1.getAbsoluteFile());
System.out.println("文件父级目录=" + file1.getParent());
System.out.println("文件大小(字节)=" + file1.length());
System.out.println("文件是否存在=" + file1.exists());
System.out.println("是不是一个文件=" + file1.isFile());
System.out.println("是不是一个目录=" + file1.isDirectory());

// 判断文件是否存在
if (file1.exists()) {
// 删除文件
if (file1.delete()) {
System.out.println("文件" + file1.getName() + "删除成功");
} else {
System.out.println("文件" + file1.getName() + "删除失败");
}
} else {
System.out.println("文件不存在,无法删除");
}

// 创建一级目录
String filePath = "E:\\demo1";
File file2 = new File(filePath);
if (file2.mkdir()) {
System.out.println("一级目录" + file2.getName() + "创建成功");
} else {
System.out.println("一级目录" + file2.getName() + "创建失败");
}

// 创建多级目录
File file3 = new File("E:\\demo1\\a\\b\\c");
if (file3.mkdirs()) {
System.out.println("多级目录" + file3.getName() + "创建成功");
} else {
System.out.println("多级目录" + file3.getName() + "创建失败");
}

// 判断文件是否存在
if (file2.exists()) {
// 只能删除空目录和文件
if (file2.delete()) {
System.out.println("目录" + file2.getName() + "删除成功");
} else {
System.out.println("目录" + file2.getName() + "删除失败");
}
} else {
System.out.println("目录不存在,无法删除");
}
}
}

IO 流原理

  1. I/O是Input/Output的缩写,I/O技术是非常实用的技术,用于处理数据传输。
    如读/写文件,网络通讯等。
  2. Java程序中,对于数据的输入/输出操作以”流(stream)”的方式进行。
  3. java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过方
    法输入或输出数据
  4. 输入input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
  5. 输出output:将程序(内存)数据输出到磁盘、光盘等存储设备中

流的分类

  • 按操作数据单位不同分为:字节流(一般用于传输二进制文件),字符流(一般用于传输文本文件)
  • 按数据流的流向不同分为:输入流,输出流
  • 按流的角色的不同分为:节点流,处理流/包装流
抽象基类 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

节点流和处理流

  1. 节点流是底层流/低级流,直接与数据源相关(例如:FileInputStream 专用于操作文件,ByteArrayInputStream 专用于操作数组,等等)
  2. 处理流又称包装流,既可以消除不同节点流之间的实现差异,也可以提供更加方便的方法来完成输入和输出。底层对节点流进行了包装,试了修饰器设计模式,不会与数据源直接相连
  • 性能的提高:主要以增加缓冲的方式来提高输入输出的效率。
  • 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入输出大批量的数据,使用更加灵活方便

节点输入输出流

文件字节节点流

FileInputStream

  1. 基本介绍
  • FileInputStream 类是 InputStream 类的子类
  • FileInputStream 类每次只读取一个字节
  1. 构造方法
1
2
3
public FileInputStream(String filePath) {
// 方法体
}
  1. 常用方法
  • public int read():从指定文件中读取一个字节,再次调用会从上次读取完的位置继续一个字节,该文件内容读取完毕会返回-1
  • public int read(byte[] b):从指定文件中读取指定字节数组大小的字节(每次读取一个字节,然后放入该字节数组中),返回读取字节的个数,读取完毕返回-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
package com.itcz;

import java.io.FileInputStream;
import java.io.IOException;

/**
* @author 奥数定理
* @version 1.0
*/
public class FileInputStreamTest {
public static void main(String[] args) {
FileInputStream fileInputStream = null;
// 单个字节读取
try {
// 读取E:\demo.txt文件下的内容,每次读取一个字节
fileInputStream = new FileInputStream("E:\\demo.txt");
int data = 0;
while ((data = fileInputStream.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

System.out.println();

// 通过字节数组读取
byte[] bytes = new byte[8];
try {
// 读取E:\demo.txt文件下的内容,每次读取一个字节
fileInputStream = new FileInputStream("E:\\demo.txt");
int dataLen = 0;
while ((dataLen = fileInputStream.read(bytes)) != -1) {
System.out.print(new String(bytes, 0, dataLen));
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

FileOutputStream

  1. 基本介绍
  • FileOutputStream 类是 OutputStream 类的子类
  • FileOutputStream 类每次只写入一个字节
  • 如果文件不存在,会创建该文件
  1. 构造方法
1
2
3
4
5
6
7
public FileOutStream(String filePath) {
// 方法体
}

public FileOutStream(String filePath, boolean append) {
// 方法体
}
  1. 常用方法
  • public void write(int byte):写入单个字节
  • public void write(byte[] b):通过字节数组,写入多个字节
  • public void write(byte[] b, int off, int len):通过指定范围(从 off 开始,写入len 个字节)的字节数组,写入多个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.FileOutputStream;
import java.io.IOException;

/**
* @author 奥数定理
* @version 1.0
*/
public class FileOutputStreamTest {
public static void main(String[] args) {
FileOutputStream fileOutputStream = null;
try {
// 默认不追加内容,会替换内容
// fileOutputStream = new FileOutputStream("E:\\demo1.txt");
// 追加内容
fileOutputStream = new FileOutputStream("E:\\demo1.txt", true);

// 1.写入单个字节
// fileOutputStream.write('H');

// 2.通过字节数组,写入多个字节
String str = "hello, world";
byte[] bytes = str.getBytes();
// fileOutputStream.write(bytes);

// 3.通过指定范围的字节数组,写入多个字节
// 例如,写入字节数组bytes中从下标为0开始,到下标为2结束的字节
fileOutputStream.write(bytes, 0, 3);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

文件字符节点流

FileReader

  1. 基本介绍
  • FileReader 类是 OutputStreamReader 类的子类,而 OutputStreamReader 类又是 Reader 类的子类
  • FileReader 类对象每次只读取一个字符,使用字符数组的方法进行读取,也是每次只读取一个字符,但是每次都从字符数组中读取字符
  1. 构造方法
1
2
3
public FileReader(File file) {}

public FileReader(String FilePaint
  1. 常用方法
  • public int write(int c):读取单个字符,文件读取完毕返回-1
  • public int write(char[] chars):读取指定字符数组大小个字符,每次将读取的字符存入字符数组中,返回读取到的字符个数,文件读取完毕返回-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
import java.io.FileReader;
import java.io.IOException;

/**
* @author 奥数定理
* @version 1.0
*/
public class FileReaderTest {
public static void main(String[] args) {
FileReader fileReader = null;

try {
fileReader = new FileReader("E:\\story.txt");
int data = 0;
while ((data = fileReader.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

System.out.println();

try {
fileReader = new FileReader("E:\\story.txt");
int dataLen = 0;
char[] chars = new char[8];
while ((dataLen = fileReader.read(chars)) != -1) {
System.out.print(new String(chars, 0, dataLen));
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
fileReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

FileWriter

  1. 基本介绍
  • FileWriter 类是 OutputStreamWriter 类的子类,而 OutputStreamWriter 类又是 Writer 类的子类
  • FileWriter 类对象每次只写入一个字符,使用字符数组的方法进行写入,也是每次只写入一个字符,但是每次都从字符数组中读取字符,然后写入文件
  • 如果文件不存在,会创建该文件
  1. 构造方法
1
2
3
4
5
public FileWriter(File file) {}

public FileWriter(String FilePath) {}

public FileWriter(File file / String filePath, boolean append) {}
  1. 常用方法
  • public void write(int c):写入单个字符
  • public void write(char[] chars):写入指定字符数组
  • public void write(char[] chars, int off, int len):写入指定范围的字符数组,从off 开始,写入 len 个字符
  • public void write(String str):写入整个字符串
  • public void write(String str, int off, int len):写入字符串的指定部分,从off 开始,写入 len 个字符

FileWriter 类的对象使用完毕后,一定要调用 flush() 或 close() 方法,不然内容无法写入文件

包装输入输出流

字符处理流

BufferedReader

  1. 基本介绍
  • 该处理流对 Reader 类下的所有字符输入流进行了封装,通过构造器接收某个字符输入流,即可对对应的数据源进行处理
  • 该类继承了 Reader 类
  1. 构造方法
1
2
3
public BufferedReader(Reader reader) {
this.reader = reader;
}
  1. 常用方法
  • public String readLine():每次读取指定文件一行的内容,每次读取从下次读取位置继续读取,文件读取完毕后返回null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

/**
* @author 奥数定理
* @version 1.0
*/
public class BufferedReaderTest {
public static void main(String[] args) {
String filePath = "E:\\story.txt";
BufferedReader bufferedReader = null;
try {
bufferedReader = new BufferedReader(new FileReader(filePath));
String dataLine = null;
// 循环读取文件,直到dataLine为null
while ((dataLine = bufferedReader.readLine()) != null) {
System.out.println(dataLine);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
// 底层会关闭 FileReader 类对应的流
bufferedReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

BufferedWriter

  1. 基本介绍
  • 该处理流对 Writer 类下的所有字符输出流进行了封装,通过构造器接收某个字符输出流,即可对对应的数据源进行处理
  • 该类继承了 Writer 类
  1. 构造方法
1
2
3
public BufferedWriter(Writer writer) {
this.writer = writer;
}
  1. 常用方法
  • public void write(String str):往对应的数据源写入字符串
  • public void newLine():往对应的数据源插入一个和操作系统相关的换行符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

/**
* @author 奥数定理
* @version 1.0
*/
public class BufferedWriterTest {
public static void main(String[] args) {
BufferedWriter bufferedWriter = null;
String filePath = "E:\\demo.txt";
try {
bufferedWriter = new BufferedWriter(new FileWriter(filePath, true));
bufferedWriter.write("hello, world");
bufferedWriter.newLine();
bufferedWriter.write("你好,世界");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
bufferedWriter.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

字节处理流

BufferedInputStream

  1. 基本介绍
  • 该类继承了 FilterInputStream 类,而 FilterInputStream 类继承了 InputStream 类
  • 该处理流对 InputStream 类下的所有字节输入流进行了封装,通过构造器接收某个字节输入流,即可对对应的数据源进行处理
  1. 构造方法
1
2
3
public BufferedInputStream(InputStream inputStream) {
this.in = inputStream;
}
  1. 常用方法
  • public int read():从指定文件中读取一个字节,再次调用会从上次读取完的位置继续一个字节,该文件内容读取完毕会返回-1
  • public int read(byte[] b):从指定文件中读取指定字节数组大小的字节(每次读取一个字节,然后放入该字节数组中),返回读取字节的个数,读取完毕返回-1

BufferedOutputStream

  1. 基本介绍
  • 该类继承了 FilterOutputStream 类,而 FilterOutputStream 类继承了 OutputStream 类
  • 该处理流对 OutputStream 类下的所有字节输出流进行了封装,通过构造器接收某个字节输出流,即可对对应的数据源进行处理
  1. 构造方法
1
2
3
public BufferedOutputStream(OutputStream outputStream) {
this.out = outputStream;
}
  1. 常用方法
  • public void write(int byte):写入单个字节
  • public void write(byte[] b):通过字节数组,写入多个字节
  • public void write(byte[] b, int off, int len):通过指定范围(从 off 开始,写入len 个字节)的字节数组,写入多个字节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.itcz;

import java.io.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class BufferStreamTest {
public static void main(String[] args) {
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
String inputPath = "E:\\logo.png";
String outputPath = "E:\\logo1.png";
try {
bis = new BufferedInputStream(new FileInputStream(inputPath));
bos = new BufferedOutputStream(new FileOutputStream(outputPath));
byte[] b = new byte[1024];
int dataLen = 0;
while ((dataLen = bis.read(b)) != -1) {
bos.write(b, 0, dataLen);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
bis.close();
bos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

对象处理流

ObjectInputStream

  1. 基本介绍
  • 该类实现了 ObjectOutput 接口和 OjbectStreamConstants 接口
  • 该处理流负责将文件中的对象的信息和类型读取到内存中(反序列化)
  1. 构造方法
1
2
3
public ObjectInputStream(InputStrem in) {
this.in = in;
}
  1. 常用方法
  • public Integer readInt(Integer num):反序列化Integer类的对象
  • public Boolean readBoolean(Boolean flag):反序列化Bollean类的对象
  • public Character readChar(Character c):反序列化Character类的对象
  • public Double readDouble(Double d):反序列化Double类的对象
  • public String readUTF(String str):反序列化字符串类型的对象
  • public Object readObject(Object obj):反序列化一个实现了 Serializable 接口或者 Externalizable 接口的类的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

/**
* @author 奥数定理
* @version 1.0
*/
public class ObjectInputStreamTest {
public static void main(String[] args) {
try (ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("E:\\demo.dat"))) {
System.out.println(objectInputStream.readInt());
System.out.println(objectInputStream.readBoolean());
System.out.println(objectInputStream.readChar());
System.out.println(objectInputStream.readUTF());
System.out.println(objectInputStream.readDouble());
Object dog = objectInputStream.readObject();
System.out.println(dog);
// Dog类为同包下的公共类
if (dog instanceof Dog) {
System.out.println(((Dog) dog).getName());
System.out.println(((Dog) dog).getAge());
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}

ObjectOutputStream

  1. 基本介绍
  • 该类实现了 ObjectInput 接口和 OjbectStreamConstants 接口
  • 该处理流负责将对象的信息和类型从内存中写入文件中(序列化)
  1. 构造方法
1
2
3
public ObjectOutputStream(OutputStrem out) {
this.out = out;
}
  1. 常用方法
  • public void writeInt(Integer num):序列化Integer类的对象
  • public void writeBoolean(Boolean flag):序列化Bollean类的对象
  • public void writeChar(Character c):序列化Character类的对象
  • public void writeDouble(Double d):序列化Double类的对象
  • public void writeUTF(String str):序列化字符串类型的对象
  • public void writeObject(Object obj):序列化一个实现了 Serializable 接口或者 Externalizable 接口的类的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;

/**
* @author 奥数定理
* @version 1.0
*/
public class ObjectOutputStreamTest {
public static void main(String[] args) {
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream("E:\\demo.dat"));
objectOutputStream.write(100);
objectOutputStream.writeBoolean(true);
objectOutputStream.writeChar('A');
objectOutputStream.writeUTF("你好,世界");
objectOutputStream.writeDouble(1.1);
// Dog类为同包下的公共类
objectOutputStream.writeObject(new Dog("小花", 3));
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
objectOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
  1. 细节
  • 对某个文件进行序列化或反序列化时,代码中读写顺序要一致
  • 序列化或反序列化的对象对应的类必须实现 Serializable 接口或者 Externalizable 接口
  • 序列化的类中建议添加SerialVersionUID属性,为了提高版本的兼容性
1
private static final long serialVersionUID = 1L;

即当该类中添加属性时,序列化或反序列化该对象时不会认为该类变成了一个全新的类,而是把它当作之前类的升级版

  • 序列化对象时,默认将里面所有属性都进行序列化,但除了static或transient修饰的成员
  • 序列化对象时,要求里面属性的类型也需要实现序列化接口
  • 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化

转换处理流

基本介绍

  • InputStreamReader:Reader 的子类,可以将 InputStream(字节流) 转换成Reader(字符流)
  • OutputStreamWriter:Writer的子类,可以将 OutputStream(字节流) 转换成Writer(字符流)
  • 当处理纯文本数据时,如果使用字符流效率更高,并且可以有效解决中文问题,所以建议将字节流转换成字符流
  • 可以在使用时指定编码格式(比如utf-8,gbk,gb2312,ISO8859-1等)

InputStreamReader

构造方法

1
2
3
public InputStreamReader(InputStream inputStream, Charset charset) {
// 方法体
}

将字节流转换为字符流,并且可以指定转换格式,这样就不会存在中文乱码问题,未指定时默认为UTF-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
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
* @author 奥数定理
* @version 1.0
*/
public class InputStreamReaderTest {
public static void main(String[] args) {
BufferedReader bufferedReader = null;
try {
// 将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("E:\\demo.txt"), "gbk");
// 再将该字符流放入到字符处理流中
bufferedReader = new BufferedReader(isr);
String data = null;
while ((data = bufferedReader.readLine()) != null) {
System.out.println(data);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
bufferedReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

OutputStreamWriter

构造方法

1
2
3
public OutputStreamReader(OutputStream outputStream, Charset charset) {
// 方法体
}

将字节流转换为字符流,并且可以指定转换格式,这样就不会存在中文乱码问题,未指定时默认为UTF-8编码格式

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

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

/**
* @author 奥数定理
* @version 1.0
*/
public class OutputStreamWriterTest {
public static void main(String[] args) throws IOException {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("E:\\demo1.txt"), "gbk"));
bufferedWriter.write("你好,世界");
bufferedWriter.close();
}
}

打印处理流

基本介绍

  • 打印流只有输出流,没有输入流
  • 打印流负责将数据输出到显示器或者文件中

PrintStream

  1. 基本介绍
  • 该处理流是字节打印处理流,System 工具类中的 out 属性就是 PrintStream 类型
  • 该处理流是 FilterOutStream 类的子类, FilterOutStream 类是 OutputStream 类的子类
  1. 常用方法
  • public void print(String str):打印字符串到指定位置
  • public void write(String str):打印字符串到指定位置
  1. 可以通过System.setOut(PrintStream printStream)方法,重定向输出设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.io.PrintStream;

/**
* @author 奥数定理
* @version 1.0
*/
public class PrintStreamTest {
public static void main(String[] args) throws IOException {
PrintStream printStream = System.out;
printStream.print("你好,世界");
printStream.write("你好,世界".getBytes());
// 重定向输出位置
System.setOut(new PrintStream("E:\\demo.txt"));
System.out.print("hello,world");
printStream.close();
}
}

PrintWriter

  1. 基本介绍:该打印流是字符打印处理流,该处理流是 Writer 类的子类
  2. 常用方法
  • public void print(String str):打印字符串到指定位置
  • public void write(String str):打印字符串到指定位置
  1. 构造方法
1
2
3
public PrintWriter(OutputStream out) {
// 方法体
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

/**
* @author 奥数定理
* @version 1.0
*/
public class PrintWriterTest {
public static void main(String[] args) throws IOException {
// 设置输出位置为E:\demo.txt文件,设置输出方式为追加方式
PrintWriter printWriter = new PrintWriter(new FileWriter("E:\\demo.txt",true));
// 也可设置输出位置为显示器
// PrintWriter printWriter = new PrintWriter(System.out);
printWriter.print("你好,世界");
printWriter.write("hello, world");
// 必须关闭该打印流,才能正式输出到指定位置
printWriter.close();
}
}

网络编程

InetAddress

  1. 基本介绍:该类主要用于获取主机或者服务器的域名和IP地址
  2. 常用方法
  • getLocalHost():获取本地主机的主机名和IP地址
  • getByName(String str):根据指定主机名/域名获取IP地址对象
  • getHostName():获取InetAddress对象的主机名
  • getHostAddress():获取InetAddress对象的IP地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
* @author 奥数定理
* @version 1.0
*/
public class InetAddressTest {
public static void main(String[] args) throws UnknownHostException {
// 1.获取本地主机的主机名和IP地址
InetAddress inetAddress = InetAddress.getLocalHost();
System.out.println(inetAddress);

// 2.根据指定主机名/域名获取IP地址对象
InetAddress inetAddress1 = InetAddress.getByName("www.baidu.com");
System.out.println(inetAddress1);
InetAddress inetAddress2 = InetAddress.getByName("ASUS");

// 3.获取InetAddress对象的主机名
String hostName = inetAddress2.getHostName();
System.out.println(hostName);

// 4.获取InetAddress对象的IP地址
String hostAddress = inetAddress2.getHostAddress();
System.out.println(hostAddress);
}
}

Socket

  1. 基本介绍
  • 在Java中服务端和客户端都通过Socket类来发送请求和接收请求,不同是的服务端还需要通过ServerSocket类来监听端口
  • 该类专用于建立TCP连接传递数据
  1. 构造方法
1
2
3
public Socket(String hostAddress, int port) {
// 方法体
}
  1. 常用方法
  • getInputStream():该方法可以获取到InputStream类的对象,通过该对象可以获取到客户端发送的数据
  • getOutputStream():该方法可以获取到OutputStream类的对象,通过该对象可以向服务端发送的数据
  • shutdownOutput():该方法用于提示对方自己的数据发送完毕,防止死锁的发生,也可以通过writer.newLine()方法写入结束标记,但是读取必须使用reader.readLine()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @author 奥数定理
* @version 1.0
*/
public class SocketServerTest {
public static void main(String[] args) throws IOException {
System.out.println("服务端启动,监听9999端口");
// 1.创建 ServerSocket 类的对象,该类用于创建服务器的Socket类的对象,每有一个客户机和服务器连接,就会生成一个Socket类的对象,如果没有客户端连接会处于堵塞状态
ServerSocket serverSocket = new ServerSocket(9999);

// 2.获取请求,打印数据到控制台
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String data = null;
if ((data = bufferedReader.readLine()) != null) {
System.out.println(data);
}

// 3.服务端向客户端发送一个请求
OutputStream outputStream = socket.getOutputStream();
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
bufferedWriter.write("hello, client");
// 写入结束标识符
bufferedWriter.newLine();
// 使用字符流,必须刷新流,否则不会写入数据
bufferedWriter.flush();

// 4.关闭流和服务端
bufferedWriter.close();
bufferedReader.close();
socket.close();
serverSocket.close();
System.out.println("服务器退出了");
}
}

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

/**
* @author 奥数定理
* @version 1.0
*/
public class SocketClientTest {
public static void main(String[] args) throws IOException {
System.out.println("客户端启动");
// 1.客户端通过连接服务器的端口,创建Socket
Socket socket = new Socket(InetAddress.getLocalHost(), 9999);

// 2.客户端发送请求
OutputStream outputStream = socket.getOutputStream();
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
BufferedWriter bufferedWriter = new BufferedWriter(outputStreamWriter);
bufferedWriter.write("hello, server");
// 写入结束标识符
bufferedWriter.newLine();
// 使用字符流,必须刷新流,否则不会写入数据
bufferedWriter.flush();

// 3.客户端接收请求
InputStream inputStream = socket.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String data = null;
while ((data = bufferedReader.readLine()) != null) {
System.out.println(data);
}

// 4.关闭流和客户端
System.out.println("客户端退出");
bufferedWriter.close();
bufferedReader.close();
socket.close();
}
}

ServerSocket

  1. 基本介绍:该类用于服务端监听端口
  2. 常用方法:public Socket accept():当客户端与服务端监听的端口建立TCP连接后,会返回一个Socket对象,否则服务端会一直堵塞在该方法中

DatagramSocket 和 DatagramPacket

基本介绍

  • 类 DatagramSocket 和 DatagramPacket(数据报)实现了基于 UDP 协议网络程序
  • UDP数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候抵达
  • DatagramPacket 类的对象封装了 UDP 数据报,在数据报中包含了发送端的IP地址和端口号以及接收端的IP地址和端口号
  • UDP 协议中每个数据报都给出了完整的地址信息,因此发送数据报前无需建立连接
  • UDP 协议中传输的数据报大小最大为64k
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.IOException;
import java.net.*;

/**
* @author 奥数定理
* @version 1.0
*/
public class UDPSender {
public static void main(String[] args) throws IOException {
System.out.println("启动客户端");
// 1.创建 DatagramSocket 类的对象,监听8888端口接收数据报
DatagramSocket socket = new DatagramSocket(8888);
// 2.发送数据报
byte[] bytes = "hello, 今天晚上吃火锅".getBytes();
// 封装UDP数据报,用于发送给服务端
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(), 9999);
// 发送数据报
socket.send(packet);
// 3.接收数据报
byte[] bytes1 = new byte[1024];
DatagramPacket packet1 = new DatagramPacket(bytes1, bytes1.length);
socket.receive(packet1);
// 获取数据报的真实长度
int length = packet1.getLength();
// 获取数据报的内容
byte[] data = packet1.getData();
// 打印数据
System.out.println(new String(data, 0, length));

// 关闭客户端
socket.close();
System.out.println("客户端退出");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

/**
* @author 奥数定理
* @version 1.0
*/
public class UDPReceiver {
public static void main(String[] args) throws IOException {
System.out.println("启动服务端");
// 1.创建 DatagramSocket 类的对象,监听9999端口
DatagramSocket socket = new DatagramSocket(9999);
// 2.接收UDP数据报
byte[] bytes = new byte[1024];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
// 如果没有数据传输到9999端口,程序会堵塞到该代码
socket.receive(packet);
// 获取数据报的真实长度
int length = packet.getLength();
// 获取数据报的内容
byte[] data = packet.getData();
// 打印数据
System.out.println(new String(data, 0, length));
// 3.发送数据报
byte[] bytes1 = "好的".getBytes();
// 封装数据报
DatagramPacket packet1 = new DatagramPacket(bytes1, bytes1.length, InetAddress.getLocalHost(), 8888);
socket.send(packet1);
// 关闭服务端
socket.close();
System.out.println("服务端退出");
}
}

反射

反射机制

  1. 基本概念
  • 反射机制允许程序在执行期借助于ReflectionAPI取得任何类的内部信息(比如成员变量,构造器,成员方法等等),并能操作对象的属性及方法。反射在设计模式和框架底层都会用到
  • 加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息。通过这个对象得到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,形象的称之为:反射
  1. 反射相关的主要类
  • java.lang.Class:类对象,Class对象标识某个类加载后在堆中的对象
  • java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法
  • java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
  • java.lang.reflect.Constructor:代表类的构造方法,Constructor对象表示构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* @author 奥数定理
* @version 1.0
*/
@SuppressWarnings({"all"})
public class Reflect01 {
public static void main(String[] args) throws Exception {
// 根据类的全路径得到该类的类对象
Class cls = Class.forName("com.itcz.Cat");
// 通过类对象获取到该类的对象
Object o = cls.newInstance();
System.out.println("o的运行类型为" + o.getClass());

// 通过类对象和方法名得到该类的方法对象
Method methodName = cls.getMethod("cry");
// 通过反射调用该方法,调用格式为:方法对象.invoke(该类的对象)
methodName.invoke(o);

// 通过该类获取该类的属性对象
// getField方法无法获取到私有属性
// Field fieldName = cls.getField("name");
Field fieldName = cls.getField("age");
// 通过反射调用该属性,调用格式:属性对象.get(该类的对象)
System.out.println(fieldName.get(o));

// 通过该类获取该类的无参方法
Constructor constructor = cls.getConstructor();
System.out.println(constructor);
// 通过该类获取该类的有参方法
Constructor constructor1 = cls.getConstructor(String.class);
System.out.println(constructor1);
}
}

class Cat {
private String name;
public int age;

public Cat (String name) {
this.name = name;
}

public Cat () {}

public void cry() {
System.out.println(name + "喵喵喵叫.......");
}
}

Class 类

  1. 基本介绍
  • Class也是类,因此也继承了Object类
  • Class类对象不是new出来的,而是系统创建的
  • 对于某个类的Class类对象,在内存中只有一份,因为类只加载一次
  • 每个类的实例都会记得自己是由哪个Class实例所生成
  • 通过Class可以完整地得到一个类的完整结构,通过一系列API
  • Class对象是存在在堆中
  • 类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据
  1. 常用方法
方法名 功能说明
static Class forName(String name) 返回指定类名 name的Class对象
Object newInstance() 调用缺省构造函数,返回该Class对象的一个实例
getName() 返回此Class对象所表示的实体(类、接口、数组类、基本类型等)名称
Class [] getInterfaces() 获取当前Class对象的接口
ClassLoader getClassLoader() 返回该类的类加载器
Class getSuperclass() 返回表示此Class所表示的实体的超类的Class
Constructor[] getConstructors() 返回一个包含某些Constructor对象的数组
Field[] getDeclaredFields() 返回Field对象的一个数组
Method getMethod(String name,Class … paramTypes) 返回一个Method对象,此对象的形参类型为paramType
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import java.lang.reflect.Field;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflect02 {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException, InstantiationException {
String classFullName = "com.itcz.Car";
// 获取到Class类对象,?表示不确定的Java类型
Class<?> cls = Class.forName(classFullName);
// 显示该Class类对象是哪个类的Class对象
System.out.println(cls);
// 显示该Class类对象的运行类型
System.out.println(Class.class);
// 返回包名
System.out.println(cls.getPackage().getName());
// 返回该Class类对象对应的类的类名
System.out.println(cls.getName());
// 通过反射创建对象
Object obj = cls.newInstance();
// 通过反射获取属性
Field brand = cls.getField("brand");
// 通过属性对象赋值
brand.set(obj, "宝马");
// 获取值
System.out.println(brand.get(obj));
// 获取该类所有的属性
Field[] fields = cls.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
}
}
class Car {
public String brand;
public int price;

public Car() {
}

public Car(String brand, int price) {
this.brand = brand;
this.price = price;
}

public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}
}
  1. 可以通过以下几种方式获取Class类对象
  • 已知一个类的全类名,且该类在类路径下,可以通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException异常,应用场景:多用于配置文件,读取类的全路径,加载类
  • 已知具体的类,可以通过类名.class获取,应用场景:多用于参数传递,比如通过反射得到对应构造器对象
  • 已知某个类的实例,通过调用该实例的getClass()方法获取Class对象,应用场景:通过创建好的对象,获取Class对象
  • 通过类加载器获取到类对象
  • 对于基本数据类型,使用 基本数据类型名.class 获取Class类对象
  • 对于基本数据类型的包装类,使用 包装类名.TYPE 获取Class类对象
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
/**
* @author 奥数定理
* @version 1.0
*/
public class Reflect03 {
public static void main(String[] args) throws ClassNotFoundException {
// 1.通过Class.forName方法
Class<?> cls1 = Class.forName("java.lang.String");
System.out.println(cls1);
// 2.通过类名.class
Class<String> cls2 = String.class;
System.out.println(cls2);
// 3.通过实例.getClass()方法
Car car = new Car();
Class<? extends Car> cls3 = car.getClass();
System.out.println(cls3);
// 4.通过类加载器
ClassLoader classLoader = car.getClass().getClassLoader();
Class<?> cls4 = classLoader.loadClass("java.lang.String");
System.out.println(cls4);
// 5.基本数据类型的类对象
Class<Integer> integerClass = int.class;
Class<Character> characterClass = char.class;
System.out.println(integerClass);
// 6.包装类的类对象
Class<Integer> type1 = Integer.TYPE;
Class<Character> type2 = Character.TYPE;
System.out.println(type2);
}
}

类加载

  1. 分类
  • 静态加载:编译时加载相关的类,如果没有则报错,依赖性太强
  • 动态加载:运行时加载需要的类,如果运行时不用该类,则不报错,降低了依赖性
  1. 加载时机
  • 当通过new关键字创建对象时,加载该类的信息,该加载属于静态加载
  • 当子类被加载时,父类也加载,该加载属于静态加载
  • 调用类中的静态成员时,该加载属于静态加载
  • 通过反射创建对象时,该加载属于动态加载
  1. 加载分为三个阶段
  • 加载阶段:JVM在该阶段的主要目的是将字节码从不同的数据源(可能是class文件、也可能是jar包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的java.lang.Class对象
  • 链接 阶段:
    • 验证:目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括:文件格式验证(是否以魔数oxcafebabe开头)、元数据验证、字节码验证和符号引用验证,可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间。
    • 准备:JVM会在该阶段对静态变量分配内存并默认初始化(对应数据类型的默认初始值,如0、OL、null、false等)。这些变量所使用的内存都将在方法区中进行分配
    • 解析:虚拟机将常量池内的符号引用替换为直接引用的过程
  • 初始化:到初始化阶段,才真正开始执行类中定义的Java程序代码,此阶段是执行()方法的过程。()方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有静态变量的赋值动作和静态代码块中的语句,并进行合并。虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
  1. Java程序执行的流程
  • 编译阶段:将java文件编译成class文件
  • 类加载阶段:将java文件中的类信息和静态属性加载到内容
  • 执行阶段:依次执行java文件中定义的语句

通过反射获取类的所有结构信息

  1. java.lang.Class类中的方法
  • getName:获取全类名
  • getSimpleName:获取简单类名
  • getFields:获取所有public修饰的属性,包含本类以及父类的
  • getDeclaredFields:获取本类中所有属性
  • getMethods:获取所有public修饰的方法,包含本类以及父类的
  • getDeclaredMethods:获取本类中所有方法
  • getConstructors:获取所有public修饰的本类的构造器
  • getDeclaredConstructors:获取本类中所有构造器
  • getPackage:以Package形式返回 包信息
  • getSuperClass:以Class形式返回父类信息
  • getlnterfaces:以Class[]形式返回接口信息
  • getAnnotations:以Annotation[]形式返回注解信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflect04 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> person = Class.forName("com.itcz.Person");
// 1. getName:获取全类名
System.out.println(person.getName());
// 2. getSimpleName:获取简单类名
System.out.println(person.getSimpleName());
// 3. getFields:获取所有public修饰的属性,包含本类以及父类的
Field[] fields = person.getFields();
for (Field field : fields) {
System.out.println("本类以及父类的public修饰的属性=" + field.getName());
}
// 4. getDeclaredFields:获取本类中所有属性
Field[] declaredFields = person.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println("本类的属性=" + declaredField.getName());
}
// 5. getMethods:获取所有public修饰的方法,包含本类以及父类的
Method[] methods = person.getMethods();
for (Method method : methods) {
System.out.println("本类以及父类的public修饰的方法=" + method.getName());
}
// 6. getDeclaredMethods:获取本类中所有方法
Method[] declaredMethods = person.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println("本类的方法=" + declaredMethod.getName());
}
// 7. getConstructors:获取所有public修饰的构造器,包含本类以及父类的
Constructor<?>[] constructors = person.getConstructors();
for (Constructor<?> constructor : constructors) {
System.out.println("本类以及父类的public修饰的构造器=" + constructor.getName());
}
// 8. getDeclaredConstructors:获取本类中所有构造器
Constructor<?>[] declaredConstructors = person.getDeclaredConstructors();
for (Constructor<?> declaredConstructor : declaredConstructors) {
System.out.println("本类的构造方法=" + declaredConstructor.getName());
}
// 9. getPackage:以Package形式返回 包信息
System.out.println(person.getPackage());
// 10.getSuperClass:以Class形式返回父类信息
System.out.println(person.getSuperclass());
// 11.getlnterfaces:以Class[]形式返回接口信息
Class<?>[] interfaces = person.getInterfaces();
for (Class<?> anInterface : interfaces) {
System.out.println(anInterface.getName());
}
// 12.getAnnotations:以Annotation[]形式返回注解信息
Annotation[] annotations = person.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
}
}

interface IA {

}

interface IB {

}

@Deprecated
class Person implements IA, IB {
// 属性
public String name;
protected int age;
String job;
private double salary;

// 构造器
public Person() {
}
public Person(String name, int age, String job, double salary) {
this.name = name;
this.age = age;
this.job = job;
this.salary = salary;
}

// 方法
public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getJob() {
return job;
}

public void setJob(String job) {
this.job = job;
}

public double getSalary() {
return salary;
}

public void setSalary(double salary) {
this.salary = salary;
}
}
  1. java.lang.reflect.Field类中的方法
  • getModifiers:以int形式返回修饰符 [说明:默认修饰符是0,public是1,private是2,protected是4,static 是8,final是16],public(1)+static(8)=9
  • getType:以Class形式返回类型
  • getName:返回属性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.lang.reflect.Field;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflect05 {
public static void main(String[] args) throws ClassNotFoundException {
// getModifiers:以int形式返回修饰符
// [说明:默认修饰符是0,public是1,private是2,protected是4,static 是8,final是16],public(1)+static(8)=9
// getType:以Class形式返回类型
// getName:返回属性名
Class<?> aClass = Class.forName("com.itcz.Dog");
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
System.out.println("本类的属性名为" + field.getName() +
" 该属性名的修饰符为" + field.getModifiers() +
" 该属性对应的数据类型为" + field.getType());
}
}
}

class Dog {
private String name;
public int age;
String hobby;
}
  1. java.lang.reflect.Method类中的方法
  • getModifiers:以int形式返回修饰符[说明:默认修饰符是0,public 是1,private是2,protected是4,static是8,final是16]
  • getReturnType:以Class形式获取 返回类型
  • getName:返回方法名
  • getParameterTypes:以Class[]返回参数类型数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.lang.reflect.Method;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflect06 {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> fish = Class.forName("com.itcz.Fish");
Method[] declaredMethods = fish.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println("Fish类的方法名=" + declaredMethod.getName() +
" 该方法的修饰符=" + declaredMethod.getModifiers() +
" 该方法的返回值类型=" + declaredMethod.getReturnType());
Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println("该方法的形参=" + parameterType);
}
}
}
}
class Fish {
public void m1(String name, int age, String hobby) {

}
protected void m2() {

}
void m3() {

}
private void m4() {

}
}
  1. java.lang.reflect.Constructor类中的方法
  • getModifiers:以int形式返回修饰符
  • getName:返回构造器名(全类名)
  • getParameterTypes:以Class[]返回参数类型数组

通过反射创建对象

  1. 方式一:调用类中的public修饰的无参构造器
  2. 方式二:调用类中的public修饰的指定构造器
  3. 方式三:调用类中的private修饰的指定构造器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import java.lang.reflect.Constructor;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflection01 {
public static void main(String[] args) throws Exception {
Class<?> userClass = Class.forName("com.itcz.reflection.User");
// 1. 方式一:调用类中的public修饰的无参构造器
// newInstance:调用类中的无参构造器,获取对应类的对象
Object o = userClass.newInstance();
System.out.println(o);

// 2. 方式二:调用类中的public修饰的指定构造器
// getConstructor(Class...clazz):根据参数列表,获取对应的public修饰的构造器对象
Constructor<?> constructor = userClass.getConstructor(String.class, int.class);
// newInstance(Object...obj):调用构造器
Object o1 = constructor.newInstance("李四", 20);
System.out.println(o1);

// 3. 方式三:调用类中非public修饰的指定构造器
// getDeclaredConstructor(Class...clazz):根据参数列表,获取对应的构造器对象
Constructor<?> declaredConstructor = userClass.getDeclaredConstructor(String.class);
// setAccessible:爆破,这样就可以通过私有构造器创建对象
declaredConstructor.setAccessible(true);
// newInstance(Object...obj):调用构造器
Object o2 = declaredConstructor.newInstance("王五");
System.out.println(o2);
}
}
class User {
private String name = "张三";
private int age = 18;

public User () {

}

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

private User (String name) {
this.name = name;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

Class类相关的方法

  • newInstance:调用类中的无参构造器,获取对应类的对象
  • getConstructor(Class…clazz):根据参数列表,获取对应的public修饰的构造器对象
  • getDeclaredConstructor(Class…clazz):根据参数列表,获取对应的构造器对象

Constructor类相关的方式

  • setAccessible:爆破,这样就可以通过私有构造器创建对象
  • newInstance(Object…obj):调用构造器

通过反射操作属性

  1. 方式一:操作public修饰的属性
  2. 方式二:操作非public修饰的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.lang.reflect.Field;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflection02 {
public static void main(String[] args) throws Exception {
Class<?> studentClass = Class.forName("com.itcz.reflection.Student");
// 1.操作public修饰的属性
Object o = studentClass.newInstance();
Field ageField = studentClass.getField("age");
ageField.set(o, 20);
System.out.println(ageField.get(o));

// 2.操作非public修饰的属性
Field nameField = studentClass.getDeclaredField("name");
// 爆破
nameField.setAccessible(true);
nameField.set(null, "李四");
System.out.println(nameField.get(null));
}
}
class Student {
public int age = 18;
private static String name = "张三";

public Student() {
}

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

Class类相关的方法:

  • getDeclaredField(String fieldName):根据属性名获取属性对象
  • getField(String fieldName):根据属性名获取public修饰的属性对象

Field类相关的方法:

  • setAccessible(boolean flag):爆破,此时就可以操作private修饰的属性
  • set(Object obj, Ojbect value):给该属性赋值
  • get(Object obj):获取该属性的值

如果是静态属性,则get和set方法中的obj参数,可以写成null

通过反射操作方法

  1. 方式一:操作public修饰的方法
  2. 方式二:操作非public修饰的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.lang.reflect.Method;

/**
* @author 奥数定理
* @version 1.0
*/
public class Reflection03 {
public static void main(String[] args) throws Exception {
Class<?> managerClass = Class.forName("com.itcz.reflection.Manager");
Object o = managerClass.newInstance();
// 操作 public 修饰的方法
Method m1 = managerClass.getMethod("m1", String.class);
m1.invoke(o, "张三");
// 操作非 public 修饰的方法
Method m2 = managerClass.getDeclaredMethod("m2", String.class, int.class, char.class);
m2.setAccessible(true);
System.out.println(m2.invoke(null, "李四", 18, '男'));
Object returnValue = m2.invoke(null, "李四", 18, '男');
System.out.println(returnValue.getClass());
}
}
class Manager {
private String name;

public void m1 (String name) {
System.out.println(name);
}

private static String m2 (String name, int age, char gender) {
return name + age + gender;
}
}

Class类相关的方法:

  • getDeclaredMethod(String methodName, Class…clazz):根据方法名和形参列表获取方法对象
  • getMethod(String methodName, Class…clazz):根据方法名和形参列表获取public修饰的方法对象

Method类相关的方法:

  • setAccessible(boolean flag):爆破,此时就可以操作private修饰的方法
  • invoke(Ojbect obj, Object…vale):调用该方法

如果是静态方法,则invoke方法中的obj参数,可以写成null

正则表达式

匹配的底层原理

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

/**
* @author 奥数定理
* @version 1.0
*/
public class RegTheory {
public static void main(String[] args) {
String content = "中共中央、国务院14日上午在人民大会堂举行2026年春节团拜会。中共中央总书记、国家主席、中央军委主席习近平发表讲话,代表党中央和国务院,向全国各族人民,向香港特别行政区同胞、澳门特别行政区同胞、台湾同胞和海外侨胞拜年。" +
"习近平强调,即将过去的乙巳蛇年,是很不平凡的一年。面对复杂多变的国际国内形势,我们迎难而上、砥砺前行,推动党和国家事业取得新进展新成效,全年经济社会发展主要目标任务顺利完成,“十四五”圆满收官,我国经济实力、科技实力、国防实力、综合国力跃上新台阶,中国式现代化迈出新的坚实步伐。" +
"  李强主持团拜会,赵乐际、王沪宁、蔡奇、丁薛祥、李希、韩正等出席。" +
"  人民大会堂宴会厅灯光璀璨、喜气洋洋,各界人士2000多人欢聚一堂、共迎佳节,现场洋溢着欢乐祥和的节日气氛。";
// 1.创建正则表达式
// 1.1. \d表示匹配所有数字,()表示分组
String regStr = "(\\d\\d)(\\d\\d)";
// 2.创建模式对象
Pattern pattern = Pattern.compile(regStr);
// 3.创建匹配器
Matcher matcher = pattern.matcher(content);
// 4.开始循环匹配
/*
(1)matcher.find()方法会从字符串中匹配符合正则表达式的子字符串,例如2026
(2)然后会将子字符串2026的第一个字符2的下标记录到groups数组的第一个元素,即groups[0] = 21
(3)会将子字符串2026的第四个字符6的下标 + 1记录到groups数组的第二个元素,即groups[1] = 25
(4)接着会将子字符串20作为第一组,将2的下标存放到groups[2],会将0的下标 + 1存放到groups[3]
(5)会将子字符串26作为第二组,将2的下标存放到groups[4],会将6的下标 + 1存放到groups[5]
(6)调用group(0)会截取字符串的21到25(不包括25)的字符串,即2026
(7)调用group(1)会截取字符串的21到23(不包括23)的字符串,即20
(8)调用group(2)会截取字符串的23到25(不包括25)的字符串,即26
(9)依次遍历匹配输出
*/
while (matcher.find()) {
// 输出匹配到的子字符串整体
System.out.println("找到:" + matcher.group(0));
// 输入匹配到的子字符串的第一组
System.out.println("第一组:" + matcher.group(1));
// 输入匹配到的子字符串的第二组
System.out.println("第二组:" + matcher.group(2));
// 输入匹配到的子字符串的第n组
// ...
}
}
}

正则表达式语法

贪婪匹配和非贪婪匹配

默认是贪婪匹配,例如字符串为”oooo”,正则表达式为”o+”,则匹配的结果为oooo

可以通过?紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,将匹配模式改为非贪婪匹配,此时匹配结果为4个o

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

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr03 {
public static void main(String[] args) {
String content = "oooo";
// 贪婪匹配
// String regStr = "o+";
// 非贪婪匹配
String regStr = "o+?";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println(matcher.group(0));
}
}
}

转义字符

在使用正则表达式去检索某些特殊字符时,需要使用转义字符,否则检索不到结果,甚至会报错,故而会使用转义字符\。例如:*+()$/?[]^{}这些字符都会使用转义字符来表达它们原来的意思

限定符

用于指定其前面的字符和组合项连续出现多少次

符号 含义 示例 说明
* 指定字符重复0次或n次(无要求)零到多 (abc)* 仅包含任意个abc的字符串,等效于\w*
+ 指定字符重复1次或n次(至少一次)1到多 m+(abc)* 以至少1个m开头,后接任意个abc的字符串
? 指定字符重复0次或1次(最多一次)0到1 m+abc? 以至少1个m开头,后接ab或abc的字符串
{n} 只能输入n个字符 [abcd]{3} 由abcd中字母组成的任意长度为3的字符串
{n,} 指定至少n个匹配 [abcd]{3,} 由abcd中字母组成的任意长度不小于3的字符串
{n,m} 指定至少n个但不多于m个匹配 [abcd]{3,5} 由abcd中字母组成的任意长度不小于3,不大于5的字符串

字符匹配符

符号 含义 示例 解释
[] 可接收的字符列表 [efgh] e、f、g、h中的任意一个字符
[^] 不接收的字符列表 [^abc] 除a、b、c之外的任意一个字符,包括数字和特殊符号
- 连字符 A-Z 任意单个大写字母
. 匹配除 \n 以外的任何字符 a..b 以a开头,b结尾,中间包括2个任意字符的长度为4的字符串
\d 匹配单个数字字符,相当于[0-9] \d{3}(\d)? 包含3个或4个数字的字符串
\D 匹配单个非数字字符,相当于[^0-9] \D(\d)* 以单个非数字字符开头,后接任意个数字字符串
\w 匹配单个数字、大小写字母字符,相当于[0-9a-zA-Z] \d{3}\w{4} 以3个数字字符开头的长度为7的数字字母字符串
\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \W+\d{2} 以至少1个非数字字母字符开头,2个数字字符结尾的字符串
\s 匹配任何空白字符(空格,制表符等) - -
\S 匹配任何非空白字符,和\s刚好相反 - -

Java正则表达式默认是区分字母大小写,可以通过以下方式不区分大小写:

  • (?i)abc 表示abc都不区分大小写
  • a(?i)bc 表示bc都不区分大小写
  • a((?i)b)c 表示b不区分大小写
  • Pattern pattern = Pattern.compile(regStr, Pattern.CASE_INSENSITIVE)

分组组合和反向引用符

捕获分组

捕获分组构造形式 说明
(pattern) 非命名捕获。捕获匹配的子字符串。编号为零的第一个捕获是由整个正则表达式模式匹配的文本,其他捕获结果则根据左括号的顺序从1开始自动编号
(?pattern) 命名捕获。将匹配的子字符串捕获到一个组名称或编号名称中。用于name的字符串不能包含任何标点符号,并且不能以数字开头,可以使用单引号代替尖括号,例如(?’name’)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr01 {
public static void main(String[] args) {
String content = "helloworld1192,nihaoshijie2290";
// 非命名捕获
// String regStr = "(\\d\\d)(\\d\\d)";
// 命名捕获
String regStr = "(?<g1>\\d\\d)(?<g2>\\d\\d)";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("匹配到的文本为:" + matcher.group(0));
System.out.println("匹配到的文本的第一个分组为:" + matcher.group(1));
System.out.println("匹配到的文本的第一个分组为(通过命名捕获):" + matcher.group("g1"));
System.out.println("匹配到的文本的第二个分组为:" + matcher.group(2));
System.out.println("匹配到的文本的第二个分组为(通过命名捕获):" + matcher.group("g2"));
}
}
}

非捕获分组

非捕获分组构造形式 说明
( ?: pattern) 匹配pattern但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用”or”字符(
( ?= pattern) 它是一个非捕获匹配。例如,‘Windows( ?= 95
( ?! pattern) 该表达式匹配不处于匹配pattern的字符串的起始点的搜索字符串。它是一个非捕获匹配。例如,‘Windows( ?! 95
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr02 {
public static void main(String[] args) {
String content = "张三老师,张三同学,张三教育";
// 1.非捕获分组,获取张三老师,张三同学,张三教育三个子字符串
// String regStr = "张三老师|张三同学|张三教育";
// String regStr = "张三(?:老师|同学|教育)";
// 2.非捕获分组,获取张三老师和张三同学中的张三
// String regStr = "张三(?=老师|同学)";
// 3.非捕获分组,获取除了张三老师和张三同学中的张三,即找到张三教育中的张三
String regStr = "张三(?!老师|同学)";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
while (matcher.find()) {
System.out.println("找到:" + matcher.group(0));
}
}
}

反向引用

圆括号的内容被捕获后,可以在这个括号后被使用,从而写出一个比较实用的匹配模式,这个称之为反向引用,这种引用既可以是在正则表达式的内部,也可以是在正则表达式的外部,内部反向引用使用 \分组号,外部反向引用使用 $分组号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.regex.Pattern;

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr07 {
public static void main(String[] args) {
String content = "我....我要....学学....学学习习习Java";
// 1.去除.
Pattern pattern = Pattern.compile("\\.");
content = pattern.matcher(content).replaceAll("");
// 2.去重
content = Pattern.compile("(.)\\1+").matcher(content).replaceAll("$1");
System.out.println("content=" + content);
}
}

选择匹配符

在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这是就可以使用选择匹配符

符号 含义 示例 解释
匹配 之前或之后的表达式

定位符

定位符,规定要匹配的字符串出现的位置,比如在字符串的开始还是结束位置

符号 含义 示例 说明
^ 指定起始字符 ^[0-9]+[a-z]* 以至少一个数字开头,后接任意个小写字母的字符串
$ 指定结束字符 ^[0-9]\-[a-z]+$ 以一个数字开头后接连字符-,并以至少一个小写字母结尾的字符串
\b 匹配目标字符串的边界 han\b 查找边界为han的子字符串,边界可以是字符串的结束位置,也可以是子串间有空格
\B 匹配目标字符串的非边界 han\B 和\b含义相反

Pattern

基本概念:pattern 对象是一个正则表达式对象。Pattern类没有公共构造方法。要创建一个Pattern对象,调用其公共静态方法,它返回一个Pattern对象。该方法接受一个正则表达式作为它的第一个参数,比如:Pattern r=Pattern.compile(pattern);

常用方法:public static boolean matches(pattern, content),该方法用于整体匹配字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr04 {
public static void main(String[] args) {
String content = "10https://www.bilibili.com/video/BV1fh411y7R8?spm_id_from=333.788.player.switch&vd_source=4ad0a2f2ad6c40766c4bfa7c008aaa3f&p=895";
String regStr = "((http|https)://)?([\\w-]+\\.)+[\\w-]+(\\/[\\w-/._?=&%#]*)?";

// 整体匹配
boolean isMatch = Pattern.matches(regStr, content);
if (isMatch) {
System.out.println("整体匹配成功");
} else {
System.out.println("整体匹配失败");
}

// 正则表达式没有添加首尾定位符,故而可以部分匹配成功
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(content);
if (matcher.find()) {
System.out.println("匹配成功");
} else {
System.out.println("匹配失败");
}
}
}

Matcher

基本概念:Matcher 对象是对输入字符串进行解释和匹配的引擎。与Pattern类一样,Matcher也没有公共构造方法。需要调用Pattern对象的matcher 方法来获得一个 Matcher对象

常用方法

方法名 说明
public int start() 返回以前匹配的初始索引
pulblic int end() 返回最后匹配字符之后的偏移量
public boolean find() 尝试查找与该模式匹配的输入序列的下一个子序列
public boolean matches() 尝试将整个区域与模式匹配
public String replaceAll(String replacement) 替换模式与给定替换模式串相匹配的输入序列的每一个子序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author 奥数定理
* @version 1.0
*/
public class RegStr05 {
public static void main(String[] args) {
String content = "123 aoshudingli hello nihao hello 123";
String regStr1 = "hello";
String regStr2 = "hello.*";
String regStr3 = "123";

// 部分匹配
Pattern pattern1 = Pattern.compile(regStr1);
Matcher matcher1 = pattern1.matcher(content);
while (matcher1.find()) {
System.out.println(matcher1.start());
System.out.println(matcher1.end());
System.out.println("找到:" + content.substring(matcher1.start(), matcher1.end()));
}

// 整体匹配
Pattern pattern2 = Pattern.compile(regStr2);
Matcher matcher2 = pattern2.matcher(content);
if (matcher2.matches()) {
System.out.println("整体匹配成功");
} else {
System.out.println("整体匹配失败");
}

// 将字符串中的所有123替换成234
Pattern pattern3 = Pattern.compile(regStr3);
Matcher matcher3 = pattern3.matcher(content);
// 该方法不会修改原字符串
String newContent = matcher3.replaceAll("234");
System.out.println("替换后的字符串为" + newContent);
}
}