目录
1.3.1 学会 Idea 下载与安装、编写第一个程序、Idea 常用配置
13.3 StringBuilder、StringBuffer、StringJoiner
18.3 synchronized 与 ReentrantLock 区别
1、Java基础入门
1.1 Java背景知识(略)
1.2 快速入门
1.2.1 学会 JDK 的下载和安装
1.2.2 CMD 常见命令
① 切盘 :盘符: ② 进入指定目录:cd : [路径] ③返回上一级目录:cd ..
④ 返回到根目录:cd / ⑤显示当前目录所有内容:dir ⑥清空屏幕:cls
1.2.3 Java入门程序步骤
编写代码(.java文件) -> 编译代码(.class文件) -> 运行代码。
1.2.4 JDK 组成
JDK(JRE(JVM+ 核心类库) + 开发工具)。
JRE : Java 运行环境。 JVM : Java 虚拟机,运行 Java 程序的地方。
1.2.5 Java跨平台的原理
因为在不同的版本的操作系统中安装不同版本的 Java 虚拟机。Java 程序的运行依赖 Java 虚拟机,从而做到一处编译,处处运行。
1.2.6 学会 JDK 环境变量配置
1.3 Java 开发工具(Idea)
1.3.1 学会 Idea 下载与安装、编写第一个程序、Idea 常用配置
1.3.2 Idea 常用快捷键
① Ctrl + D :复制当前行 ②Ctrl + Y :删除当前行 ③Ctrl + Alt + L:格式化代码
④ Alt + Shift + ↑ ↓:上下移动当前代码 ⑤:Ctrl + (Shift)+ / :注释代码
1.4 Java基础语法
1.4.1 注释
解释程序的问题,方便阅读。
注释的种类:
①单行注释:// ②多行注释:/* */ ③文档注释:/** */
1.4.2 字面量
数据在程序中的书写格式。
字面量的种类:整数、小数、字符、字符串、布尔值、空值
1.4.3 变量
用来记录程序中的数据,本质上是内存的一块区域。
变量的定义格式:
数据类型 变量名称 = 数据;
ex:int age = 18;
使用变量来记录数据,对于数据的管理更为灵活。
1.4.4 关键字
在 Java 语言中具有特殊含义的词。
1.4.5 标志符
自己取得名字,如类名,变量名。
命名规则:
1、由字母、数字、下划线以及美元符号组成。
2、不能以数字开头以及不能为 Java 关键字。
2、数据类型、运算符
2.1 数据类型
数据类型用来规定变量存储什么类型的数据。
-
基本数据类型:
- byte: 1字节
- short: 2字节
- int: 4字节
- long: 8字节
- float: 4字节
- double: 8字节
- boolean: 1字节
- char: 2字节
-
引用数据类型:
- 类(Class)
- 接口(Interface)
- 数组(Array)
2.2 数据类型转换
2.2.1 自动类型转换
自动类型转换就是数据范围小的变量可以直接赋给数据范围大的变量。
byte -> short -> int -> long -> float -> double (char -> int)
注意: 当 byte、short 、char 进行运算时会自动转为 int 类型。
2.2.2 强制类型转换
当将数据范围大的变量赋值给数据范围小的变量时会报错,此时需要进行强制类型转换。
格式:
目标数据类型 变量名 = (目标数据类型)被转换的数据类型
注意:强制类型转换可能会造成数据丢失的风险。
2.3 运算符
-
算术运算符:用于执行基本的数学运算,如加减乘除。例如,
+
(加法)、-
(减法)、*
(乘法)、/
(除法)、%
(取模)。 -
赋值运算符:用于将值赋给变量。例如,
=
(赋值)、+=
(加并赋值)、-=
(减并赋值)、*=
(乘并赋值)等。 -
比较运算符:用于比较两个值的大小关系,返回布尔类型的结果。例如,
==
(相等)、!=
(不相等)、>
(大于)、<
(小于)、>=
(大于等于)、<=
(小于等于)。 -
逻辑运算符:用于执行逻辑运算,返回布尔类型的结果。例如,
&&
(逻辑与)、||
(逻辑或)、!
(逻辑非)。 -
位运算符:用于对整数类型的数据进行位运算。例如,
&
(按位与)、|
(按位或)、^
(按位异或)、~
(按位取反)。 -
条件运算符(三元运算符):用于根据条件返回不同的值。例如,
condition ? value1 : value2
。 -
自增自减运算符:用于增加或减少变量的值。例如,
++
(自增)、--
(自减)。 -
instanceof 运算符:用于判断对象是否属于某个类。
注意:当在混合运算中,自增自减运算符放在变量后面为先运算再自增自减,放在变量前面为先自增自减再运算。
注意:运算符具有优先级,了解。
2.4 键盘导入数据
// 导包
import java.util.Scanner;
public class KeyboardInputExample {
public static void main(String[] args) {
// 创建键盘输入对象
Scanner scanner = new Scanner(System.in);
System.out.print("请输入一个整数:");
// 调用
int number = scanner.nextInt();
System.out.println("您输入的整数是:" + number);
scanner.nextLine(); // 读取换行符
System.out.print("请输入一个字符串:");
String text = scanner.nextLine();
scanner.close();
}
}
3、 程序流程控制
程序流程控制的种类:顺序结构、分支结构、循环结构。
3.1 分支结构
3.1.1 if 分支
对条件进行判断,选择执行。
格式:在Java中,if语句有三种基本形式。
if (条件表达式) {
// 如果条件为真, 执行这里的代码块
}
if (条件表达式) {
// 如果条件为真, 执行这里的代码块
} else {
// 如果条件为假, 执行这里的代码块
}
if (条件表达式1) {
// 如果条件1为真, 执行这里的代码块
} else if (条件表达式2) {
// 如果条件2为真, 执行这里的代码块
} else {
// 如果以上条件都不满足, 执行这里的代码块
}
3.1.2 switch 分支
通过比值决定执行。
格式:
switch (表达式) {
case 值1:
// 代码块1
break;
case 值2:
// 代码块2
break;
case 值3:
// 代码块3
break;
...
default:
// 默认情况的代码块
}
注意:
①表达式的类型:byte、short、int、char、String、枚举
②case值不能重复且只能为字面量。
③如果不写break则会发生穿透现象。
3.2 循环结构
3.2.1 for循环
格式:
for (初始化语句; 循环条件; 更新语句) {
// 循环体
}
执行流程:
首先执行初始化语句。接着判断循环条件是否为true,如果为false,则退出循环。如果循环条件为true,则执行循环体内的代码。执行更新语句。重复以上过程,直到循环条件为false才结束循环。
3.2.2 while循环与 do...while循环
while (条件) {
// 循环体
}
执行流程:
while
循环在执行循环体之前先检查循环条件,如果条件为真,则执行循环体,然后再次检查条件,直到条件为假时循环终止。
do {
// 循环体
} while (条件);
执行流程:
do...while
循环先执行一次循环体,然后再检查循环条件,如果条件为真,则继续执行循环体,直到条件为假时循环终止。
区别:do...while 循环会至少执行一次。
循环嵌套:一个循环包含另一个循环,外部循环每循环一次,内部循环会全部执行完一轮。
3.2.3 跳转语句
break:跳出并结束当前所在循环的执行。
continue:结束本次循环,进入下一次循环。
3.3 生成随机数
import java.util.Random;
public class RandomNumberGenerator {
public static void main(String[] args) {
Random random = new Random();
// 生成一个随机整数
int randomNumber = random.nextInt();
System.out.println("随机整数: " + randomNumber);
// 生成一个指定范围的随机整数,例如0到10之间的随机数
int randomInRange = random.nextInt(10);
System.out.println("0到10之间的随机整数: " + randomInRange);
// 生成一个随机双精度浮点数
double randomDouble = random.nextDouble();
System.out.println("随机双精度浮点数: " + randomDouble);
}
}
4、 数组
数组就是一个容器,用来存一批同类型的数据。
4.1 数组的定义和访问
4.1.1 数组的定义初始化
格式:
// 定义一个整型数组
int[] numbers = new int[5];
// 定义一个字符串数组
String[] names = new String[3];
// 静态初始化
int[] number = {1,2,3,4,5};
数组变量名中存储的是数组在内存中的地址,数组是一种引用数据类型。
4.1.2 数组的访问
// 访问第一个元素
int firstElement = numbers[0];
// 修改第三个元素的值
names[2] = "Alice";
// 数组的长度
numbers.length
注意:索引不要超出数组的范围,否则会导致ArrayIndexOutOfBoundsException
异常。
4.1.3 数组遍历
使用 for 循环或for - each遍历数组:
int[] arr = {1, 2, 3, 4, 5};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
int[] arr = {1, 2, 3, 4, 5};
for (int num : arr) {
System.out.println(num);
}
注意:当多个变量指向同一个数组时,两个变量记录的是同一个地址值,当一个变量修改数组中的元素时,另一个变量去访问数组中的元素,元素已经被修改过了。
5、 方法
是一种语法结构,把一段代码封装成一个功能,以便于重复调用。
格式:
修饰符 返回类型 方法名(参数类型 参数名) {
// 方法体
return 返回值;
}
方法需要调用才会执行,调用格式:
方法名(参数类型 参数名);
注意:方法中如果声明了具体的返回值类型,内部必须要使用 return 返回对于类型的数据。如果方法不需要接收参数,则形参列表可以不写,如果方法不需要返回值,则返回类型为void。
5.1 方法在计算中的执行原理
方法调用:当程序执行到一个方法调用的地方,会创建一个新的栈帧来保存方法需要的信息。栈帧包括方法的参数、局部变量以及方法返回的地址等信息。
方法执行:在新的栈帧中,方法的代码会被执行。这包括对参数的处理、局部变量的操作以及执行方法体中的逻辑。
内存分配:方法执行过程中,会分配内存来存储方法中的对象实例、局部变量和临时变量等。
方法返回:当方法执行完毕,会将方法的返回值返回给调用者。同时,当前的栈帧会被销毁,程序控制流程回到上一个栈帧。
栈的特点:先进后出。
5.2 Java 的参数传递机制
Java 的参数传递机制都是值传递。在 Java 中,当你传递一个基本数据类型(如int、double等)作为参数时,实际上是将该值的拷贝传递给方法。而当你传递一个对象作为参数时,实际上是将对象的引用(地址)传递给方法。
如果是基本数据类型作为参数,方法中对该参数的任何修改都不会影响原始值。而如果是对象作为参数,方法中对该对象的修改会影响原始对象,因为它们引用的是同一个对象。
5.3 方法重载
一个类中,出现多个方法名相同,但它们的形参列表不同,那么这些方法就称为方法重载。
形参列表不同包括:个数、类型、顺序不同。
用于处理一类业务中,提供多种解决方案,减少命名次数。
6、面向对象编程
开发一个个对象,把数据交给对象,再调用对象的方法来完成对数据的处理。对象本质上是一种数据结构。
6.1 认识面向对象
6.1.1 面向对象的好处
①万物皆可对象。
②对应的数据可以找对应的对象处理。
③符合人类的思维习惯,编程更简单,更直观。
④将复杂的事情变得简单。
6.1.2 对象的定义格式
类名 对象名 = new 类名();
6.2 类和对象的一些注意事项
①类名用英文驼峰命名首字母大写。
②类中定义的变量和方法成为成员变量与成员方法。
③成员变量本身存在默认值。
④一个代码文件中可以写多个class类。但只能用一个 public 修饰。修饰的类名必须为代码文件名。
⑤多个变量指向同一个对象会相互影响。
⑥如果某个对象无变量引用,则该对象无法操作,成为垃圾对象。
6.3 this关键字
this
关键字是一个指向当前对象的引用,用来拿到当前对象。
主要应用场景:
①区分实例变量与局部变量。
public class Person {
private String name;
public void setName(String name) {
this.name = name; // this.name 是实例变量,右侧name是参数
}
}
②在构造方法中调用其他构造方法(必须放在构造方法的首行)
public class Person {
private String name;
private int age;
// 无参构造调用有参构造
public Person() {
this("Unknown", 0); // 调用下方的有参构造
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
③在内部类中访问外部类实例
public class Outer {
private int x = 10;
class Inner {
void print() {
System.out.println(Outer.this.x); // 访问外部类的x
}
}
}
6.4 构造器
一种特殊的方法,方法名与类名一致,没有返回值类型。
创建对象时会去调用构造器。创建对象时,同事完成对对象成员变量的初始化赋值。
注意:
①类在设计时,如果不写构造器,会自动为类生成一个无参构造器。
②一旦有了有参构造器,Java 不会自动创建无参构造器,需要自己手动创建。
6.5 封装
面向对象的三大特征之一(封装、继承、多态)。
封装是用类设计对象处理某一个事务的数据时,应该把处理的数据以及要处理这些数据的方法,设计到同一个对象中去。
设计规范:合理隐藏、合理暴露
6.6 实体类
一种特殊的类,这个类中的成员变量都要私有化,并且要对外提供 get、set 方法。类中必须要有一个公共的无参构造器。
实体类只负责数据存取,而对数据的处理交给其他类完成,以实现数据和数据业务处理分离。
7、常用API之String、ArrayList
7.1 String(字符串)
7.1.1 字符串创建
①直接赋值:String str = "Hello, World!";
②使用String构造函数:String str = new String(
"Hello, World!");
③使用stringBuffer或StringBuilder。
7.1.2 字符串常用方法
①length()
: 返回字符串的长度。
②charAt(int index)
: 返回指定索引位置的字符。
③substring(int beginIndex)
: 返回从指定索引开始到字符串末尾的子字符串。
④substring(int beginIndex, int endIndex)
: 返回从beginIndex开始到endIndex结束的子字符串(不包括endIndex)。
⑤equals():字符串比较。
⑥equalsIgnoreCase():忽略字母大小写比较。
⑦replace(char oldChar, char newChar)
: 字符串替换。
⑧indexOf(String str)
: 返回指定子字符串在原字符串中第一次出现的索引位置。
... ...
7.1.3 字符串注意事项
①字符串 String 对象不可变:当创建字符串时会放到字符串常量池中,但对字符串进行变化时,实际上是创建了一个新的字符串对象,原本在常量池中的字符串对象并没有改变。
②只要以直接赋值的方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只会存储一份,但通过 new 方式创建的字符串对象,每 new 一次会产生一个新的对象放在堆内存中。
好处:节省内存,提高性能。
7.2 集合
是一种用用来装数据的容器,类似数组。底层为数组。
7.2.1 对比数组的好处
①数组定义完成并启动后,长度就固定了。
②集合长度大小可变。
7.2.2 ArrayList 常用API
①创建集合:ArrayList list = new ArrayList();
②添加集合数据:list.add();
③获取索引位置处的值:list.get();
④返回集合中的元素个数:list.size();
⑤删除某个索引位置处的元素值,并返回:list.remove();
⑥修改指定索引处的值并返回:list.set();
8、Static 关键字
静态,用于修饰类的成员(变量、方法、代码块和内部类)。
变量有 static 修饰:静态变量,属于类。所有实例共享同一个静态变量。静态变量在类加载时初始化,且在程序运行期间只有一份内存空间。生命周期与类的生命周期相同。
方法有 static 修饰:静态方法属于类,而不是类的实例,因此可以直接通过类名调用。通常作为工具类。
代码块有 static 修饰:静态代码块,在类加载时执行,且只执行一次。用于初始化静态变量或执行一些只需要运行一次的操作。
8.1 单例设计模式
确保一个类只有一个对象。
①饿汉式:提前创建好对象,以空间换时间。线程安全。
public class A{
// 在类加载时就创建实例
private static A a = new A();
// 私有构造函数,防止外部实例化
private A() {}
// 提供全局访问点
public static A getInstance() {
return a;
}
}
②懒汉式:在第一次使用时才创建实例。节省资源。线程不安全。
public class B{
private static B b;
// 私有构造函数,防止外部实例化
private B() {}
// 提供全局访问点,使用 synchronized 确保线程安全
public static B getInstance() {
if (b == null) {
b = new B();
}
return b;
}
}
③双检锁:改进的线程安全懒汉式单例实现方式。
public class C {
private static volatile C c;
// 私有构造函数,防止外部实例化
private C() {}
// 提供全局访问点
public static C getInstance() {
if (c == null) {
synchronized (C.class) {
if (c == null) {
c = new C();
}
}
}
return c;
}
}
9、继承
Java 中提供了一个关键字 extends,用这个关键字,可以让一个类和另一类建立起父子关系。
特点:
子类能继承父类的非私有成员。
子类的对象是由子类、父类共同完成的。
好处:减少代码的重复编写。提高了代码的复用性。
注意事项:
①权限修饰符:
Java 中有四种访问控制修饰符,它们决定了类、方法、属性的可见性:
修饰符 | 类内部 | 同一个包 | 子类(不同包) | 任何地方 |
---|---|---|---|---|
private | ✔️ | ❌ | ❌ | ❌ |
default | ✔️ | ✔️ | ❌ | ❌ |
protected | ✔️ | ✔️ | ✔️ | ❌ |
public | ✔️ | ✔️ | ✔️ | ✔️ |
②单继承
Java是单继承,不支持多继承。但支持多层继承。
Object 是所有类的祖类,任何类都是Object类的子类。
③方法重写
当子类觉得父类中的某个方法不好用,或者无法满足自己的需求时,子类重新定义父类中已有的方法。重写的方法必须与父类方法具有相同的方法名、参数列表和返回类型。
注意:
①子类重写的方法的访问权限不能比父类方法更严格。
②方法重写后,方法访问会遵循就近原则。
③可以用 super 关键字访问父类成员。
④子类构造器都会先调用父类的构造器再执行自己。
10、多态、final关键字
多态是在 继承/实现 情况下的一种现象,表现为:对象多态、行为多态。同一事务,在不同时刻呈现出来的不同形态。
多态的前提:有继承/实现关系;存在父类引用子类对象,存在方法重写。
10.1 使用多态的好处及问题
好处:
①在多态的形式下,右边的对象是解耦合的,更加便于扩展与 维护。
②定义方法时,使用父类类型的形参可以接收一切子类对象,扩展性更强,更便利。
问题:
多态下父类不能使用子类独有的方法功能。
解决:
进行强制类型转换。(向下转型)
注意:进行转换时 instanceof 关键字先进行判断类型是否一致再转换。
10.2 final 关键字
用于修饰类、方法和变量。它的作用是限制被修饰实体的行为,使其不可更改或不可继承。
①当 final
修饰变量时,表示该变量是一个常量,一旦赋值后就不能再修改。
②当 final
修饰方法时,表示该方法不能被子类重写。
③当 final
修饰类时,表示该类不能被继承。
10.3 常量
使用了 static final 修饰的成员变量就称为常量通常用于记录系统的配置信息。
命名规范:大写英文单词,多个单词用下划线连接起来。
好处:可读性、可维护性好。编译后常量会被宏替换,出现常量的地方会被替换成其记住的字面量,保证两者性能一样。
11、抽象、接口
11.1 抽象
在 Java 中有一个关键字 abstract,意为抽象的。可以用他来修饰类、成员方法。abstract 修饰的类就称为抽象类,修饰的方法就称为抽象方法。
11.1.1 抽象的注意事项、特点
①抽象类中不一定有抽象方法,有抽象方法的类一定是抽象类。
②类中该有的成员抽象类中都可以有。
③抽象类不能创建对象,作为一种特殊的类,让子类继承并实现。
④一个类继承抽象类必须重写全部抽象方法,否则这个类也必须定义为抽象类。
⑤抽象方法只有方法签名,不能写方法体。
11.1.2 抽象的适用场景和好处
更好的支持生态,父类知道子类都要做某个行为,但每个子类要做的情况不一样。父类就定义成抽象方法,交给子类去重写实现。常用于模版方法设计模式。建议父类使用 final 关键字修饰模版方法,防止子类被重写。
11.2 接口
Java 定义了一个关键字 interface,用这个关键字可以去定义一个特殊的结构:接口。是一种特殊的抽象类。并且使用 implements 关键字去实现该类。
11.2.1 接口的注意事项、特点
①接口中的方法默认是抽象方法,变量默认是常量。
②接口不能直接创建对象,必须通过实现类实例化。
③接口可以继承多个接口,类可以实现多个接口。
④如果一个类实现了多个接口,且这些接口中有相同的默认方法,必须在实现类中重写该方法以解决冲突。
⑤一个类如果又继承了父类又实现了接口,会优先使用父类。
11.2.2 抽象的适用场景和好处
弥补了类单继承的不足,一个类可以实现多个接口,增加了扩展性,让程序可以面向接口编程,可以定义通用接口,可以灵活方便的切换各种业务情况。
12、内部类、枚举、泛型
12.1 内部类
①成员内部类:定义在另一个类的内部,但没有 static
修饰的内部类。
可以访问外部类的所有成员(包括私有成员)。不能定义静态成员(除非是常量)。必须通过外部类的实例来创建内部类的对象。
class Outer {
private int outerField = 10;
class Inner {
void display() {
System.out.println("Outer field value: " + outerField);
}
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner(); // 创建内部类对象
inner.display(); // 输出: Outer field value: 10
}
}
②静态内部类:定义在另一个类的内部,并用 static
修饰的内部类。
不能直接访问外部类的非静态成员。可以直接通过外部类名访问,无需创建外部类的实例。可以定义静态成员和非静态成员。
class Outer {
private static int outerStaticField = 20;
static class StaticInner {
void display() {
System.out.println("Outer static field value: " + outerStaticField);
}
}
}
public class Main {
public static void main(String[] args) {
Outer.StaticInner inner = new Outer.StaticInner(); // 创建静态内部类对象
inner.display(); // 输出: Outer static field value: 20
}
}
③ 局部内部类:定义在方法或代码块内部的类。
只能在其定义的方法或代码块内使用。可以访问外部类的成员,但只能访问方法中的 final
或有效 final
的局部变量。不能使用访问修饰符(如 public
、private
等)。
class Outer {
void outerMethod() {
final int localVar = 30;
class LocalInner {
void display() {
System.out.println("Local variable value: " + localVar);
}
}
LocalInner inner = new LocalInner();
inner.display(); // 输出: Local variable value: 30
}
}
public class Main {
public static void main(String[] args) {
Outer outer = new Outer();
outer.outerMethod();
}
}
④匿名内部类:没有名字的内部类,通常用于实现接口或继承类,并直接创建对象。
没有类名,直接通过 new
关键字创建对象。通常用于实现接口或继承抽象类。可以访问外部类的成员,但只能访问方法中的 final
或有效 final
的局部变量。
interface Greeting {
void greet();
}
public class Main {
public static void main(String[] args) {
Greeting greeting = new Greeting() { // 匿名内部类
@Override
public void greet() {
System.out.println("Hello, World!");
}
};
greeting.greet(); // 输出: Hello, World!
}
}
12.2 枚举
一种特殊的类,用 enum 声明。第一行只能写一些合法的标识符,用逗号隔开。这些名称都是常量,并且每个常量记住的都是枚举类的一个对象。
枚举类的构造器都是私有的,因此枚举类对外不能创建对象,枚举类同时还是最终类,不能被继承。
枚举常用方法:
①values()
:返回枚举类中所有枚举常量的数组。
②valueOf(String name)
:根据枚举常量的名称返回对应的枚举常量。
③name()
:返回枚举常量的名称(字符串形式)。
④ordinal()
:返回枚举常量的序号(从 0 开始)。
12.3 泛型
定义类、接口、方法时,同时声明一个或多个类型变量。称为泛型类/接口/方法。泛型是一种语法,提供了在编译阶段约束所能操作的数据类型,自动检查。可以避免强制类型转换以及其可能出现的异常。
通配符:?。可以在使用泛型的时候代表一切类型。可以约束泛型的上下限。
上限:?extends xxx :? 为xxx或其子类。
下限:? super xxx :?为xxx或其父类。
注意:泛型是工作在编译阶段,一旦程序编译成 .class 文件,就不存在泛型了,这就是泛型擦除。泛型不支持基本数据类型,只支持对象类型。
13、常用API
13.1 Object类
Object 是 Java 中所有类的祖类,所以所有类对象都能实现该类的方法。
①toString()
:返回对象的字符串表示。
②equals():
判断两个对象是否相等。
③hashCode():
返回对象的内存地址的哈希码。
④clone():
创建并返回当前对象的一个副本。
⑤wait():
使当前线程等待。
⑥notify()
, notifyAll()
: 唤醒在此对象监视器上等待的单个或所有线程。
注意:
clone 方法是复制一个对象,并返回该对象的副本。Object 类中的 clone 被 protected 修饰,所以必须重写 clone 方法。并且需要此类实现 Cloneable
接口。
浅克隆与深克隆的区别:
clone()
方法的默认行为是 浅拷贝,即只复制对象本身,而不会复制对象内部的引用类型字段。对象内部的引用类型字段仍然指向原来的对象。
而深拷贝不仅复制对象本身,还会递归复制对象内部的引用类型字段。
13.2 包装类
把基本类型的数据包装成对象,符合 Java 中万物皆对象的思想。并且由于Java中的集合(如ArrayList、HashMap等)只能存储对象,不能存储基本数据类型,因此需要将基本数据类型的数据存入集合时,或者使用泛型等情况时就需要使用包装类。
基本数据类型及其对应的包装类:
boolean
-> Boolean
byte
-> Byte
char
-> Character
short
-> Short
int
-> Integer
long
-> Long
float
-> Float
double
-> Double
自动装箱与自动拆箱:
自动装箱:将基本数据类型自动转换为其对应的包装类对象。
自动拆箱:将包装类对象自动转换为其对应的基本数据类型。
13.3 StringBuilder、StringBuffer、StringJoiner
代表可变字符创,相当于一个容器。里面装的字符串是可变的,用来操作字符串的。
相比于 String 好处:效率更高、代码更简洁、操作大量字符串修改时节省内存空间。
异同点:
不可变 vs 可变 :String
是不可变的,而StringBuilder
和StringBuffer
是可变的,StringJoiner
也是可变的。
性能 :在单线程环境中,StringBuilder
性能最佳;在多线程环境中,StringBuffer
提供线程安全,但性能较低。
用途 :StringJoiner
专注于字符串的连接,可以自定义拼接格式。
13.4 Math
工具类,提供的都是对数据进行操作的一些方法。
常用方法:
基本数学运算:
Math.abs()
:返回参数的绝对值。Math.max()
:返回两个值中的最大值。Math.min()
:返回两个值中的最小值。Math.addExact()
:返回两个整数的和,溢出时抛出ArithmeticException
。Math.round():
四舍五入。
幂和平方根:
Math.pow(double a, double b)
:返回a
的b
次幂。Math.sqrt(double a)
:返回非负数的平方根。Math.cbrt(double a)
:返回立方根。
随机数:
Math.random()
:返回一个在0.0(包括)和1.0(不包括)之间的随机double
值。
13.5 System
一个提供系统级功能的工具类。常用方法:
System.currentTimeMillis()
:返回当前时间的毫秒数(自1970年1月1日00:00:00 UTC以来的毫秒数)。
System.nanoTime()
:返回当前时间的纳秒数,通常用于测量时间间隔。
System
.exit(0):人为终止当前Java虚拟机。
13.6 BigDecimal
用于高精度计算的工具类,可以避免浮点数运算中的精度问题。BigDecimal
类提供了对任意精度的整数和小数的支持。
常用方法:
add(BigDecimal augend)
:返回两个BigDecimal
的和。
subtract(BigDecimal subtrahend)
:返回两个BigDecimal
的差。
multiply(BigDecimal multiplicand)
:返回两个BigDecimal
的积。
divide(BigDecimal divisor, int scale, RoundingMode roundingMode)
:返回两个BigDecimal
的商,指定小数位数和舍入模式。
14、时间类
14.1 JDK 8 之前传统时间类
14.1.1 Date类
创建当前时间类:
Date date = new Date();
常用方法:
getTime()
:返回自1970年1月1日00:00:00 UTC以来的毫秒数。setTime(long time)
:设置Date
对象的时间。toString()
:返回Date
对象的字符串表示。before(Date when)
:判断当前日期是否在指定日期之前。after(Date when)
:判断当前日期是否在指定日期之后。equals(Object obj)
:比较两个Date
对象是否相等。
14.1.2 SimpleDateFormat类
用于格式化和解析日期的类,可以将Date
对象转换为字符串,或将字符串解析为Date
对象。允许用户定义日期和时间的格式。
常用方法:
format(Date date)
:将Date
对象格式化为字符串。parse(String source)
:将字符串解析为Date
对象。setTimeZone(TimeZone zone)
:设置时区。
public class SimpleDateFormatExample {
public static void main(String[] args) {
// 创建 SimpleDateFormat 对象
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 获取当前日期
Date now = new Date();
// 格式化日期
String formattedDate = sdf.format(now);
System.out.println("Formatted Date: " + formattedDate);
// 解析字符串为日期
String dateString = "2023-01-01 10:30:00";
try {
Date parsedDate = sdf.parse(dateString);
System.out.println("Parsed Date: " + parsedDate);
} catch (Exception e) {
e.printStackTrace();
}
}
}
14.1.3 Calendar类
一个抽象类,提供了对日期和时间的操作。
常用方法 :
getInstance()
:获取一个Calendar
对象,通常是GregorianCalendar
的实例。get(int field)
:获取指定字段的值(如年、月、日、小时等)。set(int field, int value)
:设置指定字段的值。add(int field, int amount)
:在指定字段上添加一个时间量(如增加天数、月份等)。roll(int field, int amount)
:在指定字段上滚动时间(不会改变更高的字段)。getTime()
:返回一个Date
对象,表示当前Calendar
的时间。
14.2 新增时间类
14.2.1 LocalDate类
表示一个没有时间部分的日期,通常用于表示年、月、日。常用方法:
now()
:获取当前日期。of(int year, int month, int dayOfMonth)
:创建指定日期的LocalDate
。plusDays(long days)
、minusDays(long days)
:返回加上或减去指定天数的日期。getDayOfWeek()
:获取星期几。isBefore(LocalDate other)
、isAfter(LocalDate other)
:比较日期。
14.2.2 LocalTime 类
表示没有日期部分的时间,通常用于表示小时、分钟、秒和纳秒。常用方法:
now()
:获取当前时间。of(int hour, int minute)
:创建指定时间的LocalTime
。plusHours(long hours)
、minusMinutes(long minutes)
:返回加上或减去指定小时或分钟的时间。getHour()
、getMinute()
:获取小时和分钟。
14.2.3 LocalDateTime 类
是LocalDate
和LocalTime
的组合,表示日期和时间,常用方法:
now()
:获取当前的日期和时间。of(int year, int month, int dayOfMonth, int hour, int minute)
:创建指定日期和时间的LocalDateTime
。plusDays(long days)
、minusHours(long hours)
:返回加上或减去指定天数或小时的日期时间。getYear()
、getMonth()
、getDayOfMonth()
:获取年份、月份、天数。
14.2.4 ZonedDateTime 类
表示日期和时间,并包含时区信息。它非常适用于全球时区的日期和时间处理。常用方法:
now()
:获取当前日期时间(包括时区)。of(LocalDateTime dateTime, ZoneId zone)
:创建指定日期时间和时区的ZonedDateTime
。plusHours(long hours)
、minusDays(long days)
:返回加上或减去指定时间单位后的ZonedDateTime
。getZone()
:获取时区。
14.2.5 Instant 类
通常用于表示时间戳,即自1970年1月1日00:00:00 UTC以来的秒数或纳秒数。常用方法:
now()
:获取当前时间戳。ofEpochSecond(long epochSecond)
:根据从1970年1月1日00:00:00 UTC以来的秒数创建Instant
。plusSeconds(long seconds)
、minusMillis(long millis)
:返回加上或减去指定时间单位后的Instant
。
14.2.6 Duration 类
表示两个时间点之间的时间间隔,常用方法:
between(Temporal startInclusive, Temporal endExclusive)
:计算两个时间点之间的间隔。toMinutes()
、toHours()
:将持续时间转换为分钟或小时。plus(long amountToAdd, TemporalUnit unit)
:返回加上指定时间单位后的Duration
。
14.2.7 DateTimeFormatter类
线程安全、不可变的类,旨在格式化和解析日期和时间。能够将日期和时间对象转换为字符串格式,并将符合格式的字符串解析回相应的日期和时间对象。常用方法:①
静态方法 :
ofPattern(String pattern)
:根据指定的模式创建一个DateTimeFormatter
实例。ISO_LOCAL_DATE
、ISO_LOCAL_TIME
、ISO_LOCAL_DATE_TIME
等:提供一些常用的标准格式化器。
实例方法 :
format(TemporalAccessor temporal)
:将日期时间对象格式化为字符串。parse(CharSequence text)
:将字符串解析为日期时间对象。parse(CharSequence text, TemporalQuery<T> query)
:将字符串解析指定的日期时间类型。
15、Lambda表达式、方法引用
15.1 Lambda表达式
用于简化匿名内部类的代码写法。基本格式为:
(被重写方法的形参列表) -> {被重写方法的方法体代码;}
参数类型可以省略不写;如果只有一个参数,()也可以省略;如果 Lambda 表达式中的方法体只有一行代码,{}可以不写,同时要省略分号,如果这行代码是 return 语句,return 也要去掉不写。
15.2 方法引用
进一步简化 Lambda 表达式。
①静态方法引用:只调用一个静态方法,前后参数的形式一致。
类名::静态方法
②实例方法引用:只调用一个实例方法,前后参数一致。
对象名::实例方法
③特定类型方法引用:只调用一个实例方法,且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是座位该实例方法入参的,则此时就可以使用特定类型的方法引用。
类型::方法
④构造器使用:只是在创建对象,并且前后参数情况一致,就可以使用构造器引用。
16、异常
在Java中,异常(Exception)是程序运行时发生的错误或意外情况。
16.1 异常的体系
异常的祖类为 Throwable 。大致分为两类:Exception 与 Error。
Exception 又分为 RuntimeException(运行时异常/非受检异常) 与其它异常。
RuntimeException:这些异常在运行时发生,通常是程序逻辑错误导致的,如空指针、数组越界等。开发者可以选择处理这些异常。常见的又数组索引越界异常,空指针异常,类型转换异常,数字格式异常等等。
其它异常:又称为受检异常,这些异常在编译时检查,通常是外部因素导致的错误,如文件不存在、网络连接失败等。开发者必须处理这些异常,否则代码无法通过编译。常见的受检异常包括 IOException
、SQLException
等。
Error:错误,通常与虚拟机相关,如内存溢出(OutOfMemoryError
)。这些错误通常无法恢复,程序会直接终止。
16.2 异常的处理机制
Java提供了 try-catch-finally
结构来处理异常。
try:用于包裹可能抛出异常的代码块。
catch:用于捕获并处理特定类型的异常。可以有多个 catch
块处理不同类型的异常。
finally:无论是否发生异常,finally
块中的代码都会执行。通常用于释放资源,如关闭文件、数据库连接等。
try {
// 可能抛出异常的代码
int result = 10 / 0; // 这里会抛出 ArithmeticException
} catch (ArithmeticException e) {
// 处理 ArithmeticException
System.out.println("除零错误: " + e.getMessage());
} finally {
// 无论是否发生异常,都会执行的代码
System.out.println("执行 finally 块");
}
或者使用throw
关键字手动抛出异常、如果方法可能会抛出受检异常,但没有在方法内部处理,那么必须在方法签名中使用 throws
关键字声明该异常。
16.3 自定义异常
Java允许开发者创建自定义异常类,通常继承自 Exception
或 RuntimeException
。
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
// 使用自定义异常
public void doSomething() throws MyCustomException {
throw new MyCustomException("这是一个自定义异常");
}
17、File、IO流
17.1 File
File
类用于表示文件和目录路径名的抽象表示。它可以用来创建、删除文件或目录,检查文件是否存在,获取文件属性等。
常用方法:
-
boolean exists()
: 判断文件或目录是否存在。 -
boolean createNewFile()
: 创建一个新文件。 -
boolean mkdir()
: 创建一个目录。 -
boolean delete()
: 删除文件或目录。 -
String getName()
: 获取文件或目录的名称。 -
String getPath()
: 获取文件或目录的路径。 -
long length()
: 获取文件的大小(字节数)。
public class FileExample {
public static void main(String[] args) {
File file = new File("example.txt");
try {
if (file.createNewFile()) {
System.out.println("文件创建成功!");
} else {
System.out.println("文件已存在。");
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("文件名: " + file.getName());
System.out.println("文件路径: " + file.getPath());
System.out.println("文件大小: " + file.length() + " bytes");
}
}
17.2 IO流
用于读写数据。Java的IO流可以分为两大类:字节流和字符流。
17.2.1 字节流
以字节为单位进行读写操作,主要用于处理二进制数据(如图片、视频等)。
InputStream
和 OutputStream
是所有字节流的基类。
常用的字节流包括:
FileInputStream
和FileOutputStream
: 用于读取和写入文件。BufferedInputStream
和BufferedOutputStream
: 提供缓冲功能,提高读写效率。
17.2.2 字符流
主要用于处理字符(16位Unicode)数据,适用于处理文本文件。
Reader
和 Writer
是所有字符流的基类。
常用字符流类:
-
FileReader
/FileWriter
: 用于文件的字符输入输出。 -
BufferedReader
/BufferedWriter
: 带缓冲区的字符流,提高读写效率。
18、多线程
线程:线程是操作系统能够进行运算调度的最小单位。一个线程在一个进程中运行。同一进程内的多个线程可以共享该进程的资源。在 Java 中通过 Thread 代表线程。
进程:进程是指在系统中正在运行的一个应用程序,是系统进行资源分配和调度的基本单位。
多线程:允许在一个程序内部同时执行多个线程。每个线程都是一个独立的执行路径,可以并发地运行,并且能够独立完成特定的任务。
18.1 创建线程的方式
①继承Thread类,重写run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread is running");
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
②实现Runnable接口。(更加灵活,因为Java只支持单继承,但可以实现多个接口。)
public class MyRunnable implements Runnable {
public void run() {
System.out.println("MyRunnable is running");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
③实现Callable接口。(可以返回结果并且能够抛出异常)
public class MyCallable implements Callable<Integer> {
public Integer call() throws Exception {
return 123;
}
public static void main(String[] args) {
MyCallable callable = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
try {
System.out.println("Result from Callable: " + futureTask.get()); // 获取结果
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
④通过线程池创建线程(可以避免频繁创建和销毁线程的开销,同时可以控制并发线程的数量,便于统一管理。)
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建一个固定大小的线程池(包含3个线程)
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务给线程池执行
for (int i = 1; i <= 10; i++) {
Runnable task = new Task(i);
executorService.submit(task); // 提交任务
}
// 关闭线程池
executorService.shutdown();
}
}
// 自定义任务类
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("任务 " + taskId + " 正在执行,线程: " + Thread.currentThread().getName());
try {
Thread.sleep(2000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskId + " 执行完成,线程: " + Thread.currentThread().getName());
}
}
18.2 线程安全问题
当多个线程同时操作同一共享资源的时候就有可能出现线程安全问题。
常见解决方法:
①使用同步机制:如synchronized
关键字、ReentrantLock
类等,来保护对共享资源的访问。
②使用线程安全的数据结构:如ConcurrentHashMap
、 Hashtabl
e、
CopyOnWriteArrayList
等
③使用原子类:如AtomicInteger
、AtomicLong
等,提供了非阻塞的原子操作。
18.3 synchronized 与 ReentrantLock 区别
特性 | synchronized | ReentrantLock |
---|---|---|
锁类型 | 隐式锁 | 显式锁(手动调用lock() 和unlock()方法 ) |
可中断性 | 不支持 | 支持 |
公平锁 | 仅非公平锁 | 支持公平锁与非公平锁 |
条件变量 | 单一隐式条件 | 支持多个条件(newCondition() ) |
锁尝试 | 不支持 | 支持tryLock() 和超时获取 |
性能 | 现代JVM优化后较好 | 高竞争场景下更优 |
代码复杂度 | 简单 | 复杂(需手动管理锁) |
18.4 synchronized 锁升级
锁有四种状态,按照从低到高的顺序分别是:无锁、偏向锁、轻量级锁和重量级锁。
锁升级流程:
1、无锁 -> 偏向锁:当一个线程第一次访问同步代码块时,JVM会将锁标记为偏向锁,并记录下当前线程ID。
2、偏向锁 -> 轻量级锁:如果有另一个线程试图获取已经被偏向的锁,则会发生偏向锁撤销,并且锁会升级为轻量级锁。
3、轻量级锁 -> 重量级锁:随着竞争加剧,若多个线程反复争夺同一把锁,JVM会将锁升级为重量级锁,以避免频繁的自旋操作带来的CPU浪费。
18.5 线程通信
线程通信是指多个线程之间如何传递信息、协调执行顺序以确保程序的正确性和可预测性。
可以通过以下几种常见的机制来实现线程通信:
-
共享内存:线程通过共享对象的内存空间来进行通信。线程可以读取和修改共享对象的变量,从而实现数据交换。
-
线程同步:确保多个线程以正确的顺序访问共享资源,避免数据竞争和不一致。
-
线程通信机制:如
wait/notify
、Condition
、BlockingQueue
等,通过这些机制实现线程间的协调和通知。
经典模型:生产者与消费者模型:
public class ProducerConsumerWithWaitNotify {
private final int[] buffer = new int[1]; // 一个简单的缓冲区
private boolean available = false; // 标记缓冲区是否可用
// 生产者方法
public synchronized void produce(int value) throws InterruptedException {
while (available) { // 如果缓冲区已满,生产者等待
System.out.println("Buffer is full. Producer is waiting...");
wait(); // 释放锁并等待
}
buffer[0] = value; // 生产
available = true; // 标记缓冲区可用
System.out.println("Produced: " + value);
notify(); // 唤醒等待的消费者
}
// 消费者方法
public synchronized void consume() throws InterruptedException {
while (!available) { // 如果缓冲区为空,消费者等待
System.out.println("Buffer is empty. Consumer is waiting...");
wait(); // 释放锁并等待
}
int value = buffer[0]; // 消费
available = false; // 标记缓冲区为空
System.out.println("Consumed: " + value);
notify(); // 唤醒等待的生产者
}
public static void main(String[] args) {
ProducerConsumerWithWaitNotify pc = new ProducerConsumerWithWaitNotify();
// 创建生产者线程
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.produce(i);
Thread.sleep((int) (Math.random() * 1000)); // 模拟生产时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Producer interrupted");
}
});
// 创建消费者线程
Thread consumerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pc.consume();
Thread.sleep((int) (Math.random() * 1000)); // 模拟消费时间
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("Consumer interrupted");
}
});
producerThread.start();
consumerThread.start();
}
}
18.6 线程池
线程池就是一个可以复用和管理线程的技术。一般采用自定义线程池,不用 executors 工具类创建。通过ThreadPoolExecutor
类来实现自定义线程池。
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程的空闲存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
有七个参数:核心线程数、最大线程数、临时线程存活时间、临时线程存活时间单位、任务队列、线程工厂、拒绝策略。
执行流程:
如果线程池中的核心线程数量未满,线程池会尝试创建新的核心线程来执行任务。
如果核心线程已经全部忙碌,线程池会尝试将任务放入任务队列。
如果任务队列已满,线程池会尝试创建新的非核心线程来执行任务。
如果当前线程数量已经达到最大线程数,则会执行拒绝策略。
线程池的拒绝策略:
默认的拒绝策略,直接抛出异常,拒绝新任务。
由提交任务的线程直接执行任务,不会抛出异常。
直接丢弃新任务,不会抛出异常。
丢弃任务队列中等待最久的任务,然后尝试将新任务加入队列。
常用方法:
1、execute(Runnable task)
:提交一个不需要返回值的任务。
2、submit(Callable task)
:提交一个有返回值的任务,并返回一个Future
对象,通过该对象可以获取任务的执行结果。
3、shutdown()
:启动线程池的关闭过程,不再接受新任务,但会等待已提交的任务完成。
4、shutdownNow()
:尝试立即关闭线程池,取消所有未执行的任务,并尝试中断正在执行的任务。
线程的生命周期:
NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
新建、就绪、阻塞、等待、延迟等待、死亡
19、网络通信
基本通信架构:
①C/S:client(客户端)/server(服务端)
②B/S:Browser(浏览器)/server(服务端)
19.1 网络通信三要素
①IP地址:是网络中设备的唯一标识,用于在网络中定位设备并实现数据的正确传输。
②端口号:用于标识设备上的特定应用程序或服务。它是一个16位的数字。
③协议:连接和数据在网络中传输的规则。
19.2 IP
在Java中,IP地址的应用主要通过 InetAddress 类来实现的。常用方法:
①获取本机 IP 地址对象:InetAddress.getLocalHost();
②获取指定 IP 或者域名的 IP 地址:InetAddress.getByName();
③判断当前主机与对应主机是否能够连接:isReachable();
19.3 端口
被规定为一个16位的二进制。范围是 0 ~ 65535.
分类:
①知名端口:0-1023,通常被分配给标准服务和系统级进程。
②注册端口:1024-49151,这些端口通常被分配给用户或应用程序。
③动态端口:49152-65535,这些端口通常用于临时用途或私有应用程序。
我们自己开发的程序一般选择使用注册端口且一个设备中不能出现两个程序的端口号一样。
19.4 协议
OSI 模型层次 | TCP/IP 模型层次 | 常见协议 |
---|---|---|
应用层 | 应用层 | HTTP, HTTPS, FTP, SMTP, DNS, Telnet, SSH |
表示层 | 数据加密(如SSL/TLS)、数据压缩、数据转换等 | |
会话层 | NetBIOS, RPC, SDP, PPTP, L2TP, SSH (部分功能) | |
传输层 | 传输层 | TCP, UDP, SCTP, DCCP |
网络层 | 互联网层 | IP (IPv4, IPv6), ICMP, IGMP, OSPF |
数据链路层 | 链路层 | Ethernet, Wi-Fi(IEEE 802.11), PPP, ARP, RARP, L2TP(部分功能) |
物理层 | 链路层或直接映射到物理层 | 定义电气、机械过程以及硬件规范(例如电缆、连接器类型) |
传输层的两个重要协议区别:
特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
连接类型 | 面向连接 | 无连接 |
可靠性 | 提供可靠的数据传输 | 不保证数据包的到达 |
速度 | 相对较慢,因为有建立连接、确认和重传机制 | 更快,因为它不需要建立连接或等待确认 |
使用场景 | 适用于需要高可靠性、对延迟不敏感的应用,如网页浏览、电子邮件 | 适用于实时应用,如视频会议、在线游戏、语音通话等 |
数据处理 | 数据被视为字节流,没有明确的消息边界 | 每个UDP数据报文都是独立的,保持了消息的边界 |
握手过程 | 建立连接需要三次握手(SYN, SYN-ACK, ACK) | 不需要握手,直接发送数据 |
TCP协议的三次握手与四次挥手:
三次握手主要用于在客户端与服务器之间建立TCP连接。这个过程确保双方都准备好进行通信,并同意开始一个会话。
步骤 | 方向 | 描述 |
---|---|---|
1 | 客户端 -> 服务器 | 发送SYN(同步序列编号),表示请求建立连接 |
2 | 服务器 -> 客户端 | 回复SYN-ACK(确认字符),表示已收到请求并同意建立连接 |
3 | 客户端 -> 服务器 | 发送ACK(确认字符),表示收到服务器的确认,连接建立 |
四次挥手用于安全地关闭TCP连接。此过程确保所有数据都被正确接收,并允许任一方先发起关闭请求。
步骤 | 方向 | 描述 |
---|---|---|
1 | 主动方 -> 被动方 | 发送FIN(结束序列编号),表示请求关闭连接 |
2 | 被动方 -> 主动方 | 回复ACK(确认字符),表示已收到关闭请求,但可能还有未传输完的数据 |
3 | 被动方 -> 主动方 | 当被动方准备好关闭时发送FIN,表示自己也准备关闭连接 |
4 | 主动方 -> 被动方 | 发送ACK,确认对方的FIN,连接正式关闭 |
20、反射、注解、动态代理
20.1 反射
反射就是加载类,允许以剖析的编程方式解剖类中的各种成分。
假设已经有了一个 Person 类:
①加载类,获取类的字节码(class对象)
// 方法1: 使用类字面量
Class<?> personClass1 = Person.class;
// 方法2: 使用Class.forName(),需要提供类的完整包路径
Class<?> personClass2 = Class.forName("fully.qualified.package.Person");
// 方法3: 如果你已经有了一个该类的对象,可以使用getClass()
Person personInstance = new Person();
Class<?> personClass3 = personInstance.getClass();
②获取构造器并创建实例
try {
// 获取特定参数类型的构造器
Constructor<?> constructor = personClass1.getConstructor(String.class, int.class);
// 创建新实例
Object personObj = constructor.newInstance("John Doe", 30);
System.out.println(personObj.toString());
} catch (Exception e) {
e.printStackTrace();
}
③获取和修改成员变量
try {
// 获取私有字段name
Field nameField = personClass1.getDeclaredField("name");
nameField.setAccessible(true); // 访问私有字段
// 暴力反射
// 修改字段值
nameField.set(personInstance, "Jane Doe");
System.out.println(personInstance.toString());
} catch (Exception e) {
e.printStackTrace();
}
④调用方法
try {
// 获取getName方法
Method getNameMethod = personClass1.getMethod("getName");
String name = (String) getNameMethod.invoke(personInstance);
System.out.println("Name: " + name);
// 调用setName方法
Method setNameMethod = personClass1.getMethod("setName", String.class);
setNameMethod.invoke(personInstance, "Tom");
System.out.println(personInstance.toString());
} catch (Exception e) {
e.printStackTrace();
}
反射的作用:
①可以得到一个类中的全部成分然后操作。
②可以破坏封装性。
③可以做框架。
20.2 注解
注解是 Java 代码里的特殊标记,作用是让其他程序根据注解信息来决定怎么执行该程序,注解可以用在类、构造器、方法上等,是一种特殊的接口。通过 @interface
关键字来声明。
自定义注解:
public @interface 注解名称{
// 注解元素定义
}
可以在注解类型内定义成员变量,这些成员变量实际上就是注解的元素。它们看起来像方法声明,并且可以有默认值。
public @interface MyCustomAnnotation {
String author() default "unknown";
String date();
int revision() default 1;
}
可以使用元注解来指定注解的行为,如生命周期、适用范围等。常见的两个元注解:
①@Retention
:指示注解保留的时期
②@Target
:指定注解适用的目标元素类型
20.3 动态代理
动态代理允许在运行时创建一个实现了一组接口的代理实例。动态代理通常用于AOP(面向切面编程)、日志记录、事务管理等场景中,以增强或修改现有类的行为而不改变其实现代码。
动态代理主要通过 Proxy
类和 InvocationHandler
接口来实现。
实现步骤:
-
定义接口:首先需要有一个业务接口,代理将基于该接口进行操作。
-
创建真实主题类:实现上述接口的真实主题类,代表实际执行业务逻辑的对象。
public interface Service { void doSomething(); } public class ServiceImpl implements Service { @Override public void doSomething() { System.out.println("Doing something..."); } }
-
创建
InvocationHandler
实现类:编写一个类实现InvocationHandler
接口,并在invoke
方法中定义当代理对象上的方法被调用时应执行的操作。public class LoggingHandler implements InvocationHandler { private Object target; public LoggingHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Method " + method.getName() + " is called."); Object result = method.invoke(target, args); // 调用目标对象的方法 System.out.println("Method " + method.getName() + " finished."); return result; } }
-
使用
Proxy.newProxyInstance()
创建代理对象:根据提供的类加载器、接口数组以及调用处理器创建代理对象。public class Main { public static void main(String[] args) { Service realService = new ServiceImpl(); LoggingHandler handler = new LoggingHandler(realService); Service proxyInstance = (Service) Proxy.newProxyInstance( realService.getClass().getClassLoader(), realService.getClass().getInterfaces(), handler); proxyInstance.doSomething(); // 通过代理对象调用方法 } }