第一章 Java 基础
1. Hello World程序
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
2. 数据类型
Java提供了多种数据类型,包括基本数据类型和引用数据类型。基本数据类型用于存储简单的数据值,而引用数据类型用于存储对象的引用。
2.1 基本数据类型
Java的基本数据类型包括整数类型、浮点类型、字符类型和布尔类型。每种基本数据类型都有固定的字节大小和表示范围。
| 数据类型 | 字节大小 | 数据范围 |
|---|---|---|
| byte | 1 | -128 到 127 |
| short | 2 | -32,768 到 32,767 |
| int | 4 | -2,147,483,648 到 2,147,483,647 |
| long | 8 | -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 |
| float | 4 | 单精度浮点数,可精确到小数点后7位 |
| double | 8 | 双精度浮点数,可精确到小数点后15位 |
| char | 2 | Unicode字符,范围为 ‘\u0000’ 到 ‘\uffff’ |
| boolean | 1 | true 或 false |
2.2 自动类型转换和强制类型转换
在Java中,基本数据类型之间存在自动类型转换和强制类型转换的机制。
-
自动类型转换:当将一个小范围的数据类型赋值给一个大范围的数据类型时,会发生自动类型转换不会丢失精度。类型转换从小到大的顺序如下:
byte->short->int->long->float->double
整数类型自动提升为更大的整数类型:
byte->short->int->long
浮点数类型的自动浮点数类型:
float->double
如果将一个较大范围的数据类型转换为较小范围的数据类型,会发生数据丢失的情况,需要进行强制类型转换。
-
强制类型转换:将一个大范围的数据类型转换为小范围的数据类型,强制类型转换可能会导致数据丢失或溢出。
例如,将一个
long类型的数据转换为int类型,将丢失long类型的高32位数据。将一个
double类型的数据转换为int类型,将丢失double类型的小数部分。
2.3 引用数据类型
Java中的引用数据类型用于存储对象的引用,而不是直接存储对象本身。引用数据类型包括类(Class)、接口(Interface)和数组(Array)。
- 类(Class):类是一种自定义的数据类型,用于创建对象。类定义了对象的属性和方法,并可以根据类创建多个对象。
class Person {
String name;
int age;
void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
- 接口(Interface):接口是一种抽象数据类型,定义了一组方法的规范。类可以实现接口,并提供实现接口中定义的方法。
interface Drawable {
void draw();
}
- 数组(Array):数组是一种用于存储多个相同类型数据的容器。数组可以是一维、二维或多维的,并且具有固定的大小。
int[] numbers = {1, 2, 3, 4, 5};
String[] names = {"Alice", "Bob", "Charlie"};
引用数据类型在栈内存中存储对象的引用地址,而对象本身存储在堆内存中。通过引用操作对象的属性和调用对象的方法。
Person person = new Person();
person.name = "Alice";
person.age = 25;
person.display(); // 调用对象的方法
需要注意的是,引用数据类型的赋值实际上是将引用复制给另一个引用变量,而不是复制对象本身。
Person person1 = new Person();
Person person2 = person1; // 将person1的引用复制给person2
2.4 标识符命名规范和注释
在Java中,标识符用于命名变量、方法、类等程序元素。标识符必须遵循一定的命名规范:
- 标识符由字母、数字、下划线和美元符号组成。
- 标识符的第一个字符不能是数字。
- 标识符区分大小写。
- 标识符不能是Java的关键字或保留字。
3. 变量和常量
3.1 变量
变量是用于存储数据的内存位置。在Java中,变量具有特定的类型,并且可以在程序执行过程中改变其值。
-
声明变量
在Java中,声明变量需要指定变量的类型和名称。语法格式如下:
type variableName;其中,
type表示变量的数据类型,variableName表示变量的名称。例如,声明一个整数变量:int age; -
初始化变量
变量可以在声明时初始化,也可以在稍后的代码中进行初始化。初始化变量即为变量赋予初始值。示例:
-
int age = 25; // 在声明时初始化变量
或者:
int age; // 声明变量 age = 25; // 在稍后的代码中初始化变量需要注意的是,赋值时整数类型默认类型是int,浮点类型默认为double。示例:
// 错误示例:直接将 int 类型的字面值赋值给 byte 类型的变量 byte age = 25; // 编译错误,需要进行类型转换 // 正确示例:使用后缀进行赋值 byte age = 25B; // 使用 B 后缀表示字节型字面值,正确赋值Java 还支持使用后缀表示特定类型的字面值:
B后缀:用于表示字节型(byte)。S后缀:用于表示短整型(short)。L后缀:用于表示长整型(long)。F后缀:用于表示单精度浮点型(float)。D后缀:用于表示双精度浮点型(double)。双精度浮点数字面值默认为double类型,但可以使用D后缀显式指定。
-
-
变量的作用域
变量的作用域指的是变量在程序中可见的范围。在Java中,变量的作用域可以是方法内部、方法参数、代码块或类的成员变量。
- 方法内部变量:在方法内部声明的变量只在方法内部可见。
void calculate() { int result = 0; // 方法内部变量 // ... }- 方法参数:方法参数是在方法声明中指定的变量,用于接收调用方法时传递的参数。
void printName(String name) { // ... }- 代码块变量:在代码块中声明的变量只在代码块内部可见。
{ int count = 0; // 代码块变量 // ... }- 类的成员变量:类的成员变量属于类的实例,可以在整个类中访问。
class Person { String name; // 类的成员变量 // ... } -
类变量和实例变量
在Java中,类的成员变量可以分为类变量和实例变量。
- 类变量:类变量是使用
static关键字声明的变量,它属于整个类而不是类的实例。类变量在类的所有实例之间共享。
class Counter { static int count; // 类变量 // ... }- 实例变量:实例变量是在类中声明的非静态变量,每个类的实例都有自己的一份实例变量。
class Person { String name; // 实例变量 int age; // 实例变量 // ... } - 类变量:类变量是使用
3.2 常量
常量是在程序执行过程中不可更改的值。在Java中,使用关键字final声明常量。常量一旦被赋值后,其值不能再被修改。
-
声明常量
声明常量需要使用
final关键字,并遵循命名规范。通常使用大写字母表示常量的名称。final dataType CONSTANT_NAME = value;其中,
dataType表示常量的数据类型,CONSTANT_NAME表示常量的名称,value表示常量的初始值。示例:final double PI = 3.14159; -
常量的命名规范
常量的命名规范与变量的命名规范相似,但常量通常使用全大写字母,并使用下划线分隔单词。
final int MAX_VALUE = 100; -
常量的使用
常量可以在程序中使用,但不能修改其值。常量的主要作用是定义程序中的固定值,提高代码的可读性和维护性。
final int MAX_VALUE = 100; int number = 50; if (number > MAX_VALUE) { // ... }
4. 常见的标识符命名场景
- 类名接口名:使用大写字母开头的驼峰命名法(CamelCase),例如
Person,Student,Runnable,Comparable。类名通常用于表示具体的对象类型,接口名通常用于表示一组相关的操作或功能。 - 方法名:使用小写字母开头的驼峰命名法,例如
getName(),calculateTotal(),isInitialized()。方法名应具有描述性,能清晰地表达方法的功能或操作。 - 变量名:使用小写字母开头的驼峰命名法,例如
age,firstName,totalCount。变量名应具有描述性,能清晰地表示变量所代表的含义。 - 常量名:通常使用全大写字母,单词间用下划线分隔的命名法,例如
MAX_VALUE,PI,DEFAULT_TIMEOUT。常量名应具有描述性,表达常量的含义和用途。 - 包名:使用小写字母,单词间用点号(.)分隔的命名法,例如
com.example.myproject,org.openai.chatbot. 包名应具有唯一性和可读性,能够反映所包含类的层次结构和作用域。 - 构造函数:与类名相同,使用大写字母开头的驼峰命名法,例如
Person(),Student(),MyClass()。构造函数用于创建对象,与类名相同能够清晰地表示所创建的对象类型。 - 枚举类型:与类名相同的规则适用于枚举类型,使用大写字母开头的驼峰命名法,例如
Color.RED,DayOfWeek.MONDAY,Size.SMALL。
5. 运算符
运算符是用于执行特定操作的符号或关键字。在Java中,有多种类型的运算符,包括算术运算符、赋值运算符、比较运算符、逻辑运算符、位运算符和其他运算符。本节将介绍这些运算符的使用和优先级。
- 算术运算符
| 运算符 | 描述 |
|---|---|
| + | 加法 |
| - | 减法 |
| * | 乘法 |
| / | 除法 |
| % | 取模运算 |
- 赋值运算符
| 运算符 | 描述 |
|---|---|
| = | 简单赋值 |
| += | 加法赋值 |
| -= | 减法赋值 |
| *= | 乘法赋值 |
| /= | 除法赋值 |
| %= | 取模赋值 |
- 比较运算符
| 运算符 | 描述 |
|---|---|
| == | 相等 |
| != | 不等 |
| > | 大于 |
| < | 小于 |
| >= | 大于等于 |
| <= | 小于等于 |
- 逻辑运算符
| 运算符 | 描述 |
|---|---|
| && | 短路与 |
| || | 短路或 |
| ! | 非 |
- 位运算符
| 运算符 | 描述 |
|---|---|
| & | 位与 |
| | | 位或 |
| ^ | 位异或 |
| ~ | 位非 |
| << | 左移 |
| >> | 右移 |
| >>> | 无符号右移 |
- 关系运算符
| 运算符 | 描述 |
|---|---|
| == | 相等 |
| != | 不等 |
| > | 大于 |
| < | 小于 |
| >= | 大于等于 |
| <= | 小于等于 |
- 递增和递减运算符
| 运算符 | 描述 |
|---|---|
| ++ | 递增 |
| – | 递减 |
- 三目运算符
| 运算符 | 描述 |
|---|---|
| ? : | 条件运算符(三目运算符) |
- 运算符的优先级
在表达式中,运算符具有不同的优先级。具有较高优先级的运算符会先于具有较低优先级的运算符进行计算。如果存在相同优先级的运算符,会根据结合性决定计算顺序。
下表按照优先级从高到低列出了常见的运算符:
| 运算符 | 描述 |
|---|---|
| () | 括号(用于改变运算符的优先级) |
| ++, – | 递增和递减运算符 |
| ! | 逻辑非运算符 |
| *, /, % | 乘法、除法和取模运算符 |
| +, - | 加法和减法运算符 |
| <<, >>, >>> | 左移、右移和无符号右移运算符 |
| <, <=, >, >= | 关系运算符 |
| ==, != | 相等性运算符 |
| & | 位与运算符 |
| ^ | 位异或运算符 |
| | | 位或运算符 |
| && | 短路与运算符 |
| || | 短路或运算符 |
| ?: | 条件运算符(三目运算符) |
| =, +=, -=, *=, /=, %= | 赋值运算符 |
以上表格中的运算符顺序是按照优先级从高到低排列的,具有相同优先级的运算符按照结合性从左到右计算。
请注意,赋值运算符(包括简单赋值运算符和复合赋值运算符)是右结合性的,而不是按照从左到右的结合性。
右结合性意味着赋值运算符计算顺序是从右往左进行的。
例如,考虑以下示例:
int a = 5;
int b = 10;
int c = 15;
c = a = b;
System.out.println("a = " + a); // 输出结果:a = 10
System.out.println("b = " + b); // 输出结果:b = 10
System.out.println("c = " + c); // 输出结果:c = 10
6. 控制流程
在编程中,流程控制语句用于控制程序的执行流程,根据条件进行判断和重复执行特定的代码块。Java 提供了多种流程控制语句,包括条件语句、循环语句和跳转语句。
6.1 条件语句
条件语句根据条件的真假来选择性地执行代码块。
-
if-else 语句
if (条件1) { // 如果条件1为真,执行这里的代码块 } else if (条件2) { // 如果条件1不满足,且条件2为真,执行这里的代码块 } else { // 如果前面的条件都不满足,执行这里的代码块 }条件1条件2是一个布尔表达式,用于判断执行哪个代码块。- 如果
条件1为真,将执行if代码块中的代码。 - 如果
条件1为假条件2为真,将执行else if代码块中的代码。 - 如果
条件1为假条件2为假时,将执行else代码块中的代码。
-
switch 语句
switch (表达式) { case 值1: // 如果表达式的值等于值1,执行这里的代码块 break; case 值2: // 如果表达式的值等于值2,执行这里的代码块 break; default: // 如果表达式的值与前面的值都不匹配,执行这里的代码块 break; }- 根据
表达式的值,选择匹配的case分支执行相应的代码块。 - 如果找到匹配的分支,则执行该分支的代码块,并使用
break语句跳出switch语句。 - 如果没有找到匹配的分支,则执行
default分支的代码块(可选)。 - 需要注意的是,如果缺少
break语句,执行匹配的语句后会向下顺序执行。
- 根据
6.2 循环语句
循环语句用于重复执行特定的代码块,直到满足退出条件为止。
-
while 循环
while (条件) { // 只要条件为真,重复执行这里的代码块 }条件是一个布尔表达式,用于判断是否继续执行循环。- 只要
条件为真,就会重复执行while循环中的代码块。 - 在循环执行代码块之前和之后,都会检查
条件的值。如果条件为假,则退出循环,继续执行后续的代码。
-
do-while 循环
do { // 先执行这里的代码块 } while (条件);条件是一个布尔表达式,用于判断是否继续执行循环。- 先执行
do代码块中的代码,然后再检查条件的值。 - 只要
条件为真,就会重复执行do-while循环中的代码块。 - 在循环执行代码块之前和之后,都会检查
条件的值。如果条件为假,则退出循环,继续执行后续的代码。
-
for 循环
for (初始化语句; 条件; 更新语句) { // 只要条件为真,重复执行这里的代码块 }初始化语句用于初始化循环控制变量。条件是一个布尔表达式,用于判断是否继续执行循环。更新语句用于更新循环控制变量的值。- 在执行完
初始化语句后,先检查条件的值。只要条件为真,就会重复执行for循环中的代码块。 - 在每次循环结束后,执行
更新语句来更新循环控制变量的值。
6.3 跳转语句
跳转语句用于在程序执行过程中改变执行的顺序。
- break 语句
break 语句用于跳出当前的循环或 switch 语句。
while (条件) {
if (某个条件) {
break; // 跳出循环
}
// 其他代码
}
switch (表达式) {
case 值1:
// 代码块
break; // 跳出 switch 语句
case 值2:
// 代码块
break; // 跳出 switch 语句
default:
// 代码块
break; // 跳出 switch 语句
}
- continue 语句
continue 语句用于跳过当前循环中剩余的代码,直接进入下一次循环。
for (int i = 0; i < 10; i++) {
if (i % 2 == 0) {
continue; // 跳过当前循环的剩余代码,进入下一次循环
}
// 其他代码
}
-
return 语句
return语句用于从方法中返回值,并终止方法的执行。
public int calculateSum(int a, int b) {
int sum = a + b;
return sum; // 返回 sum 的值并结束方法的执行
}
- 在方法中使用
return语句可以返回一个值,并将该值传递给调用方法的地方。 return语句也可以用于提前终止方法的执行,即使没有返回值。
7. 数组
数组是一种用于存储多个相同类型元素的数据结构。在 Java 中,数组具有固定长度,并且可以在声明时或运行时初始化。使用数组可以方便地管理和操作大量数据。
7.1 声明和初始化数组
在 Java 中,声明数组需要指定数组的类型和长度。数组的长度确定后,无法再次更改。
-
声明数组
// 声明一个整型数组 int[] numbers; // 声明一个字符串数组 String[] names; -
静态初始化
静态初始化是指在声明数组的同时,直接为数组元素赋值。
// 静态初始化整型数组
int[] numbers = {1, 2, 3, 4, 5};
// 静态初始化字符串数组
String[] names = {"Alice", "Bob", "Charlie"};
- 动态初始化
动态初始化是指在声明数组后,通过指定数组长度来分配内存空间,并逐个为数组元素赋初值。
// 动态初始化整型数组
int[] numbers = new int[5];
numbers[0] = 1;
numbers[1] = 2;
numbers[2] = 3;
numbers[3] = 4;
numbers[4] = 5;
// 动态初始化字符串数组
String[] names = new String[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";
7.2 访问数组元素
数组元素的访问通过索引(下标)来实现,索引从 0 开始,依次递增。
int[] numbers = {1, 2, 3, 4, 5};
int firstNumber = numbers[0]; // 访问第一个元素
int thirdNumber = numbers[2]; // 访问第三个元素
7.3 遍历数组
可以使用 length 属性获取数组的长度,该属性返回数组中元素的个数。
再使用循环结构遍历数组,访问数组的每个元素。
- for 循环遍历数组
int[] numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.length; i++) {
int number = numbers[i]; // 获取当前元素
// 执行相应操作
}
- 增强型 for 循环遍历数组
增强型 for 循环(也称为 for-each 循环)适用于遍历数组的所有元素。
int[] numbers = {1, 2, 3,4, 5};
for (int number : numbers) {
// 执行相应操作,number 为当前元素的值
}
7.4 多维数组
除了一维数组,Java 还支持多维数组,即数组中包含数组。
-
声明和初始化多维数组
// 声明一个二维整型数组 int[][] matrix; int[] matrix2[]; // 声明一个三维字符串数组 String[][][] cube;多维数组的初始化可以通过嵌套的方式进行。
// 静态初始化二维整型数组 int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 静态初始化三维字符串数组 String[][][] cube = { { {"A1", "A2"}, {"A3", "A4"} }, { {"B1", "B2"}, {"B3", "B4"} } }; -
访问多维数组元素
通过多个索引来访问多维数组的元素。
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; int element = matrix[1][2]; // 访问第二行第三列的元素 -
遍历多维数组
可以使用嵌套的循环结构遍历多维数组的所有元素。
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { int element = matrix[i][j]; // 获取当前元素 // 执行相应操作 } }
7.5 数组的常见操作
在使用数组时,还可以进行一些常见的操作,如复制数组、排序数组等。
-
复制数组
可以使用
System.arraycopy()方法或使用循环逐个复制数组元素来复制数组。int[] source = {1, 2, 3, 4, 5}; int[] target = new int[source.length]; // 使用 System.arraycopy() 方法复制数组 System.arraycopy(source, 0, target, 0, source.length); // 使用循环逐个复制数组元素 for (int i = 0; i < source.length; i++) { target[i] = source[i]; } -
排序数组
可以使用
Arrays.sort()方法对数组进行排序。int[] numbers = {5, 3, 1, 4, 2}; Arrays.sort(numbers); // 对数组进行排序
7.6 数组的注意事项
- 在使用数组前,应确保数组已经被正确初始化,没有正确初始化数组或访问了一个为
null的数组引用,会导致空指针异常的发生。 - 数组在声明时需要指定类型和长度,且长度一旦确定后无法更改。
- 数组可以存储任意类型的元素,包括基本数据类型和引用数据类型,但是只能存储一种数据类型。
- 二维数组实际上一维数组,数组中每个元素存放指向一维数组的引用。
8. 方法
方法是一段封装了特定功能的代码块,可以通过方法名和参数列表来调用执行。在 Java 中,方法用于实现代码的模块化、重用和组织。本节将介绍方法的声明、调用、参数、返回值、重载和递归等相关内容。
8.1 方法的声明和定义
方法的声明和定义包括方法名、参数列表、返回类型和方法体的结构。方法的声明告诉编译器方法的存在和如何调用它,方法的定义则提供了方法的具体实现。
// 方法的声明
返回类型 方法名(参数列表) {
// 方法体
// 执行相应操作
}
// 方法的定义
返回类型 方法名(参数列表) {
// 方法体
// 执行相应操作
return 返回值; // 可选,如果方法有返回值的话
}
其中:
- 返回类型指定了方法执行后的返回结果类型,可以是基本数据类型或引用数据类型。如果方法没有返回值,返回类型应为
void。 - 参数列表是一组以逗号分隔的参数,用于接收调用方法时传递的值。参数列表可以为空,也可以包含一个或多个参数。
- 方法体包含了实现方法功能的代码块。
示例:
// 无返回值的方法,不带参数
public void greet() {
System.out.println("Hello, world!");
}
// 有返回值的方法,带参数
public int add(int a, int b) {
int sum = a + b;
return sum;
}
8.2 方法的调用
调用方法是通过方法名和参数列表来执行方法的过程。调用方法时,根据方法名和参数列表的匹配来确定要调用的方法。
// 调用无返回值的方法
方法名(参数列表);
// 调用有返回值的方法
数据类型 变量名 = 方法名(参数列表);
示例:
// 调用无返回值的方法
greet();
// 调用有返回值的方法
int result = add(3, 5);
System.out.println("Result: " + result);
8.3 方法的参数
方法的参数用于接收调用方法时传递的值。参数列表是一组以逗号分隔的参数,每个参数由参数类型和参数名组成。方法在执行时可以使用这些参数进行计算和处理。
// 无参数的方法
public void methodName() {
// 执行相应操作
}
// 带参数的方法
public void methodName(参数类型 参数名) {
// 执行相应操作,可以使用参数进行计算和处理
}
示例:
// 带参数的方法
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
// 调用带参数的方法
greet("John");
8.4 方法的返回值
方法的返回值是方法执行后的结果,可以是基本数据类型或引用数据类型。在方法定义时,通过返回类型来指定方法的返回值类型。方法可以使用 return 语句将结果返回给调用方。
// 无返回值的方法
public void methodName() {
// 执行相应操作
}
// 有返回值的方法
public 返回类型 methodName() {
// 执行相应操作
return 返回值;
}
示例:
// 有返回值的方法
public int add(int a, int b) {
int sum = a + b;
return sum;
}
// 调用有返回值的方法
int result = add(3, 5);
System.out.println("Result: " + result);
8.5 方法的重载
方法重载是指在同一个类中可以定义多个同名但参数列表不同的方法。通过方法的参数列表的不同来区分不同的方法,可以根据不同的需求来实现类似功能但具有不同参数的方法。
方法重载的条件:
- 方法名相同。
- 参数列表不同(参数个数、参数类型或参数顺序不同)。
示例:
// 重载的方法
public void greet() {
System.out.println("Hello!");
}
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
public void greet(String name, int age) {
System.out.println("Hello, " + name + "! You are " + age + " years old.");
}
8.6 方法的递归
方法递归是指在方法体内部调用自身的过程。递归方法可以用于解决需要重复执行相似操作的问题。在递归过程中,每次调用方法时都会将参数不断传递下去,直到达到递归终止条件才会停止递归。
递归方法必须包含递归终止条件,否则会导致无限递归,最终抛出 StackOverflowError 异常。
示例:
public int factorial(int n) {
if (n == 0 || n == 1) {
return 1; // 递归终止条件
} else {
return n * factorial(n - 1); // 递归调用自身
}
}
8.7 方法的作用域
方法的作用域指的是方法内部声明的变量的可见范围。在方法中声明的变量只能在方法内部访问,不能在其他方法中直接使用。
方法的作用域可以分为两种情况:
- 局部变量:在方法内部声明的变量,只在方法内部有效。
- 方法参数:作为方法的输入,只在方法内部有效。
示例:
public void methodName() {
int localVar = 10; // 方法内的局部变量
System.out.println(localVar);
}
public void methodName() {
int localVar = 10; // 方法内的局部变量
System.out.println(localVar);
}
public void anotherMethod(int parameter) {
System.out.println(parameter);
}
public void main(String[] args) {
methodName(); // 调用方法,输出:10
int value = 20;
anotherMethod(value); // 调用方法,输出:20
}
在上面的示例中,methodName 方法内部声明了一个局部变量 localVar,只能在该方法内部访问。而 anotherMethod 方法有一个参数 parameter,该参数只在该方法内部有效。在 main 方法中调用了这两个方法,并传递了相应的参数。通过方法调用,可以在方法内部访问局部变量和方法参数。
请注意,方法的作用域仅限于方法内部,无法在其他方法中直接使用。每个方法都有自己独立的作用域,变量的生命周期仅限于所属的方法内部。
8.8 方法的可变参数
可变参数是指方法的参数个数是可变的,可以接受不定数量的参数。在方法的参数列表中使用三个连续的点(…)表示可变参数。
可变参数的特点:
- 可变参数必须是方法参数列表中的最后一个参数。
- 可变参数可以接受任意数量的参数,包括零个参数。
- 在方法内部,可变参数被当作数组来处理。
示例:
public void printNumbers(int... numbers) {
for (int num : numbers) {
System.out.println(num);
}
}
// 调用方法
printNumbers(1, 2, 3); // 输出:1 2 3
printNumbers(); // 输出:(无输出)
8.9 方法的重写
方法重写是指在子类中重新定义父类中已有的方法。重写的方法具有相同的方法名、参数列表和返回类型。子类通过重写父类的方法来改变方法的实现逻辑,以满足子类的特定需求。
重写方法的要求:
- 方法名、参数列表和返回类型与父类方法相同。
- 子类方法的访问修饰符不能比父类方法的访问修饰符更严格(可以更宽松)。
- 子类方法的返回类型必须是父类方法返回类型的子类型(或相同类型)。
- 子类方法不能抛出比父类方法更宽泛的受检异常。
示例:
// 父类
public class Animal {
public void eat() {
System.out.println("Animal is eating.");
}
}
// 子类
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating bones.");
}
}
8.10 方法的重载与重写的区别
方法重载和方法重写是两个不同的概念和用途。
方法重载(Overloading)指的是在同一个类中可以定义多个同名但参数列表不同的方法。方法重载通过参数列表的不同来区分方法,可以根据不同的需求来实现类似功能但具有不同参数的方法。
方法重写(Overriding)指的是在子类中重新定义父类中已有的方法。重写的方法具有相同的方法名、参数列表和返回类型。通过重写父类的方法,子类可以改变方法的实现逻辑,以满足子类的特定需求。
区别:
- 方法重载发生在同一个类中,方法重写发生在子类中。
- 方法重载是通过参数列表的不同来区分方法,方法重写是通过继承关系来定义父类和子类之间的方法关系。
- 方法重载是在编译时静态决定调用哪个方法,方法重写是在运行时动态决定调用哪个方法
8.11 方法的访问修饰符
方法可以使用不同的访问修饰符来限制对方法的访问权限。Java 提供了多种访问修饰符,每种修饰符具有不同的访问级别和作用范围。
常用的方法访问修饰符包括:
public:可以被任意类访问。protected:可以被同一包内的类和子类访问。private:只能在同一类内部访问。- 默认修饰符(无修饰符):可以被同一包内的类访问。
示例:
public class MyClass {
public void publicMethod() {
// 可以被任意类访问
}
protected void protectedMethod() {
// 可以被同一包内的类和子类访问
}
private void privateMethod() {
// 只能在同一类内部访问
}
void defaultMethod() {
// 可以被同一包内的类访问
}
}
8.12 方法的静态与实例
方法可以分为静态方法和实例方法两种类型。
静态方法(Static Method)属于类本身,可以通过类名直接调用,无需创建类的实例对象。静态方法中不能访问非静态成员变量和非静态方法,只能访问静态成员变量和静态方法。
实例方法(Instance Method)属于类的实例对象,需要先创建类的实例对象,然后通过对象调用方法。实例方法可以访问类的实例变量和静态变量,以及其他实例方法和静态方法。
示例:
public class MyClass {
// 静态方法
public static void staticMethod() {
// 执行相应操作
}
// 实例方法
public void instanceMethod() {
// 执行相应操作
}
}
8.13 方法的参数传递
在方法调用过程中,参数可以按值传递给方法。Java 中的参数传递是将实际参数的值复制一份给方法的形式参数,方法内部对形式参数的修改不会影响实际参数的值。
对于基本数据类型的参数,传递的是值的副本,对形式参数的修改不会影响实际参数的值。
对于引用数据类型的参数,传递的是引用的副本,即引用的内存地址,对形式参数的修改会影响实际参数所指向的对象。
示例:
public void modifyValue(int value) {
value = 100; // 修改形式参数的值,不影响实际参数的值
}
public void modifyArray(int[] array) {
array[0] = 100; // 修改形式参数所指向的对象,会影响实际参数所指向的对象
}
// 调用方法
int x = 10;
modifyValue(x);
System.out.println(x); // 输出:10
int[] arr = {1, 2, 3};
modifyArray(arr);
System.out.println(arr[0]); // 输出:100
8.14 方法的返回值传递
方法的返回值传递指的是方法通过返回值将结果传递给调用方。返回值可以是基本数据类型或引用数据类型。
对于基本数据类型的返回值,返回的是值的副本,方法内部对返回值的修改不会影响调用方的变量。
对于引用数据类型的返回值,返回的是引用的副本,即引用的内存地址,方法内部对返回值所指向对象的修改会影响调用方持有的引用。
示例:
public int calculate() {
int result = 10;
return result; // 返回基本数据类型的值的副本
}
public int[] createArray() {
int[] array = {1, 2, 3};
return array; // 返回引用数据类型的引用的副本
}
// 调用方法
int x = calculate();
System.out.println(x); // 输出:10
int[] arr = createArray();
arr[0] = 100;
System.out.println(arr[0]); // 输出:100
9. 注释
在Java中,注释用于解释和说明代码的作用。Java支持三种类型的注释:
- 单行注释:以
//开头,注释内容在//后面的部分。
// 这是一个单行注释
int x = 5; // 可以在代码行的末尾添加注释
- 多行注释:以
/*开头,以*/结尾,注释内容在/*和*/之间。
/*
这是一个
多行注释
*/
int y = 10;
- 文档注释:以
/**开头,以*/结尾,用于生成文档。
/**
* 这是一个文档注释示例。
* 用于对方法、类或接口进行详细的描述。
* 可以包含参数说明、返回值说明等。
*
* @param x 参数x的说明
* @return 返回值的说明
*/
public int myMethod(int x) {
// 方法体
return x * 2;
}```
文档注释可以通过工具生成API文档,提供给其他开发者参考和使用。
Java中使用注释来提供代码的解释和说明。注释不会被编译器处理,注释对于代码的可读性和维护性非常重要,应该养成良好的注释习惯,对关键代码进行适当的注释解释。
第二章:面向对象编程
1. 类和对象
1.1 类的定义和声明
类是面向对象编程的基本概念之一,它是对象的模板或蓝图,描述了对象的属性和行为。类的定义和声明包括以下要点:
- 类的语法:使用关键字
class来定义一个类,紧接着是类的名称和类体。 - 类的名称:类的名称应使用大写字母开头,遵循驼峰命名法。
- 类的成员:类的成员包括成员变量和成员方法。
- 成员变量:成员变量是类的属性,用于存储对象的状态。它们可以是基本数据类型或引用数据类型。
- 成员方法:成员方法是类的行为,用于执行特定的操作。它们定义在类中,并且可以被对象调用。
示例:
public class MyClass {
// 成员变量
private int myVariable;
// 成员方法
public void myMethod() {
// 方法体
}
}
1.2 对象的创建和使用
对象是类的实例,通过类创建出来并在程序中使用。对象的创建和使用包括以下要点:
- 使用关键字
new创建对象:使用new关键字后跟类名和括号,可以创建类的实例。 - 对象的赋值:将对象赋值给变量,使变量引用该对象。
- 对象的方法调用:使用对象引用调用对象的方法。
示例:
MyClass myObject = new MyClass(); // 创建 MyClass 类的对象
myObject.myMethod(); // 调用对象的方法
1.3 成员变量和成员方法
成员变量和成员方法是类的两个重要组成部分,用于定义类的属性和行为。
- 成员变量:成员变量定义在类中,可以被类的所有方法访问。它们用于存储对象的状态,并可以在类的任何方法中使用。
- 成员方法:成员方法定义了类的行为和操作。它们可以访问类的成员变量,并可以在对象上执行特定的操作。
示例:
public class MyClass {
// 成员变量
private int myVariable;
// 成员方法
public void myMethod() {
// 使用成员变量
System.out.println("My variable: " + myVariable);
}
}
1.4 构造方法和实例化对象
构造方法是一种特殊的方法,用于创建和初始化对象。在实例化对象时,构造方法会被自动调用。构造方法的特点包括:
- 构造方法的名称与类名相同。
- 构造方法没有返回类型,甚至没有
void。 - 构造方法可以重载,即可以有多个构造方法,它们具有不同的参数列表。
- 构造方法可以用于初始化成员变量的初始值,执行特定的操作,或者接受外部传入的参数。
示例:
public class MyClass {
private int myVariable;
// 默认构造方法
public MyClass() {
myVariable = 0; // 初始化成员变量
}
// 带参数的构造方法
public MyClass(int value) {
myVariable = value; // 使用参数初始化成员变量
}
}
// 实例化对象
MyClass obj1 = new MyClass(); // 使用默认构造方法创建对象
MyClass obj2 = new MyClass(10); // 使用带参数的构造方法创建对象
1.5 方法重载
方法重载是指在一个类中定义多个方法,它们具有相同的名称但不同的参数列表。方法重载的特点包括:
- 方法重载可以根据不同的参数类型、参数个数或参数顺序来区分。
- 方法重载可以有不同的返回类型。
- 方法重载可以提高代码的复用性和灵活性。
示例:
public class MyClass {
public void myMethod() {
// 方法没有参数
}
public void myMethod(int num) {
// 方法接受一个整数参数
}
public void myMethod(String str) {
// 方法接受一个字符串参数
}
}
MyClass obj = new MyClass();
obj.myMethod(); // 调用方法 myMethod()
obj.myMethod(10); // 调用方法 myMethod(int)
obj.myMethod("Hello"); // 调用方法 myMethod(String)
1.6 类和对象的内存分配
在Java中,类和对象的内存分配是由JVM(Java虚拟机)负责的。JVM使用一种称为Java内存模型(Java Memory Model)的规范来管理内存的分配和使用。
1.6.1 JVM 内存模型概述
JVM内存模型定义了JVM在运行时如何划分和管理内存。它包括以下几个主要的内存区域:
- 方法区(Method Area):存储类的信息和静态变量。
- 堆(Heap):存储对象的实例变量。
- 栈(Stack):存储方法调用和局部变量。
- 常量池(Constant Pool):存储常量和符号引用。
- 本地方法栈(Native Method Stack):用于支持Java程序调用本地方法。
1.6.2 方法区(Method Area)
方法区是JVM的一部分,用于存储类的信息和静态变量。它在JVM启动时被创建,并且在JVM关闭时销毁。方法区是线程共享的,所有线程都可以访问其中的类信息和静态变量。
方法区存储了以下内容:
- 类的字节码(Class Bytecode):即编译后的Java类文件。
- 常量池(Constant Pool):存储类中的常量和符号引用。
- 类的静态变量(Static Variables):类级别的静态变量,不依赖于对象的创建。
- 类的方法(Methods):类中定义的方法。
1.6.3 堆(Heap)
堆是用于存储对象实例的内存区域。在Java中,所有的对象都存储在堆中,包括通过关键字new创建的对象和数组。
堆内存的特点包括:
- 动态分配:在运行时根据对象的创建和销毁进行内存的分配和释放。
- 对象共享:多个引用可以指向同一个对象,实现对象的共享。
1.6.4 栈(Stack)
栈是用于存储方法调用和局部变量的内存区域。每个线程都有自己的栈,用于记录方法的调用和执行过程。
栈内存的特点包括:
- 方法调用:栈通过方法调用的方式来管理程序的执行流程,每次方法调用会创建一个新的栈帧。
- 局部变量:栈帧中包含了方法的局部变量和临时变量。
1.6.5 常量池(Constant Pool)
常量池是存储常量和符号引用的内存区域。它包含了类中的常量、字符串字面值、类和接口的全限定名、字段和方法的符号引用等信息。
常量池的特点包括:
- 存储常量:常量池可以存储整型、浮点型、字符型、布尔型等基本数据类型的常量。
- 存储字符串字面值:字符串字面值在编译时会被放入常量池,从而实现字符串的共享。
- 存储符号引用:常量池存储了类和接口的全限定名、字段和方法的符号引用,用于在运行时解析符号引用。
1.6.6 对象的创建和销毁过程
在Java中,对象的创建和销毁是由JVM自动管理的。当通过new关键字创建对象时,JVM会执行以下步骤:
- 分配内存:JVM在堆中分配一块内存用于存储对象的实例变量。
- 初始化对象:JVM对对象进行初始化,包括初始化实例变量和调用构造方法。
- 返回引用:将分配的内存地址作为引用返回,可以通过引用来操作和访问对象。
对象的销毁是由JVM的垃圾回收机制自动处理的。当对象不再被引用时,JVM会自动标记该对象为垃圾,然后在适当的时机回收其所占用的内存。
1.6.7 对象的引用和垃圾回收
在Java中,对象通过引用来进行操作和访问。对象的引用是指向对象的指针,它存储了对象在内存中的地址。
Java中的引用类型包括:
- 普通引用:通过
new关键字创建的对象引用。 - 静态引用:指向静态变量的引用,存在于方法区中。
- 弱引用、软引用和虚引用:用于对对象进行特殊的引用控制和垃圾回收。
垃圾回收是JVM自动管理内存的过程,通过标记-清除算法和可达性分析等机制来判断哪些对象是不再被引用的垃圾对象,并释放其所占用的内存。
1.7 内部类
在Java中,内部类是指定义在其他类内部的类。内部类可以访问外部类的成员,并且可以提供更加灵活的代码组织和封装。
1.7.1 内部类的分类
内部类可以分为以下几种类型:
- 静态内部类(Static Inner Class):定义在外部类内部但被 static 修饰的内部类。静态内部类可以直接通过外部类名访问,并且不依赖于外部类的实例。
- 成员内部类(Member Inner Class):定义在外部类内部且不被 static 修饰的内部类。成员内部类的实例必须依赖于外部类的实例。
- 局部内部类(Local Inner Class):定义在方法内部或代码块内部的内部类。局部内部类只在其所在的方法或代码块内部可见,对外部是隐藏的。
- 匿名内部类(Anonymous Inner Class):没有名字的内部类,通常用作接口实现或继承父类并重写方法的简洁方式。
1.7.2 内部类的特点和用途
内部类具有以下特点和用途:
- 内部类可以访问外部类的私有成员,包括私有字段和私有方法。
- 内部类可以实现类之间的多重继承,即一个类可以继承自多个类。
- 内部类可以实现封装和隐藏,将相关的类和接口组织在一起。
- 内部类可以提供更好的代码结构和可读性,将相关的功能放在一起。
- 内部类可以实现回调和事件处理等功能。
1.7.3 内部类的语法和使用
内部类的语法格式如下:
class OuterClass {
// 外部类的成员和方法
class InnerClass {
// 内部类的成员和方法
}
}
使用内部类时,需要先创建外部类的实例,然后再通过外部类实例来创建内部类的实例。例如:
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
内部类的访问修饰符可以是 public、protected、default(包级访问)或 private,用来控制内部类的可见性。
1.7.4 内部类的示例
下面是一个使用内部类的示例,演示了不同类型的内部类的定义和使用:
public class OuterClass {
private int outerField;
public void outerMethod() {
System.out.println("Outer Method");
}
// 静态内部类
public static class StaticInnerClass {
public void innerMethod() {
System.out.println("Static Inner Method");
}
}
// 成员内部类
public class MemberInnerClass {
public void innerMethod() {
System.out.println("Member Inner Method");
}
}
public void localClassMethod() {
// 局部内部类
class LocalInnerClass {
public void innerMethod() {
System.out.println("Local Inner Method");
}
}
LocalInnerClass inner = new LocalInnerClass();
inner.innerMethod();
}
public void anonymousClassMethod() {
// 匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Anonymous Inner Method");
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
在上面的示例中,我们定义了一个外部类 OuterClass,并在其中定义了四种类型的内部类:静态内部类 StaticInnerClass、成员内部类 MemberInnerClass、局部内部类 LocalInnerClass 和匿名内部类。通过外部类的实例,我们可以访问和使用这些内部类。
通过上述的示例,我们可以更好地理解和使用内部类,提高代码的灵活性和可读性。
1.7.5 内部类的注意事项
在使用内部类时,需要注意以下几点:
- 内部类的访问修饰符可以控制其可见性。
- 内部类可以访问外部类的成员,包括私有成员。
- 内部类的实例必须依赖于外部类的实例。
- 内部类不能定义静态成员,但可以定义静态常量。
- 内部类可以实现接口和继承父类。
- 内部类的命名规范与外部类相同,但可以使用外部类的名称作为前缀进行区分。
使用内部类时,需要根据具体的需求和场景来选择适合的内部类类型,以实现代码的组织和封装。
1.7 工具类
工具类是一种常用的类设计模式,用于封装一组相关的静态方法,这些方法通常是通用的、与特定对象无关的实用功能。
工具类的特点包括:
静态方法:工具类中的方法通常是静态方法,可以直接通过类名调用,无需创建对象。
- 不可实例化:工具类通常会将构造方法私有化,以防止被实例化。因为它们的目的是提供一组静态方法,而不是作为对象的容器。
- 无状态:工具类的静态方法通常不会维护任何状态信息,即不会改变类的成员变量。它们是无状态的,仅根据输入参数进行计算和处理。
工具类的应用场景包括:
- 提供通用的算法和函数:例如数学计算、日期时间处理、字符串操作等。
- 封装复杂的逻辑:例如数据校验、文件处理、网络请求等。
- 提供工具方法集合:例如集合操作、类型转换、文件路径处理等。
2. 封装
封装是面向对象编程的核心概念之一,它将数据和方法组合在一个单元中,形成类(Class)。封装通过隐藏对象的内部细节,提供了一种对外界隐藏实现细节的方式,使得对象的使用者只需要关注对象的公共接口,而无需了解对象的内部实现。
2.1 封装的优势
封装提供了以下几个优势:
- 信息隐藏:封装允许将对象的内部数据和方法隐藏起来,只暴露必要的接口给外部访问,从而保护对象的内部状态不受外部干扰。
- 提高安全性:通过封装可以限制对对象的访问权限,只有通过指定的方法才能修改对象的状态,从而确保数据的安全性和一致性。
- 简化调用者的操作:封装将复杂的内部实现细节隐藏起来,使得调用者只需调用简单的方法即可完成操作,提高了代码的可读性和可维护性。
- 提供代码重用:通过定义可复用的类,可以在不同的项目中重用已封装好的对象和方法,减少代码的重复编写。
2.2 访问控制修饰符
在Java中,访问控制修饰符用于控制类、方法和变量的访问权限。Java提供了四种访问控制修饰符:
- public:公共访问修饰符,可以被任何类访问。
- private:私有访问修饰符,只能在当前类内部访问。
- protected:受保护的访问修饰符,可以被同一包内的类以及子类访问。
- 默认(无修饰符):默认访问修饰符,只能在同一包内访问。
通过使用这些访问控制修饰符,可以控制类的成员(变量和方法)的可见性和访问权限,从而实现对类的封装。
2.3 属性和方法的封装
在类的封装中,常用的做法是将类的属性(成员变量)声明为私有(private),并提供公共的方法(getter和setter)来访问和修改属性的值。这样做的好处是可以通过方法来控制对属性的访问,增加了对属性的保护和安全性。
示例:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0) {
this.age = age;
} else {
System.out.println("年龄不能为负数");
}
}
}
在上述示例中,name和age属性被声明为私有(private),外部无法直接访问。通过提供公共的getter和setter方法,可以实现对属性的访问和修改。在setAge方法中,还添加了对年龄的合法性进行检查,确保年龄不能为负数。
3. 继承
继承是面向对象编程中一种重要的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。通过继承,子类可以直接使用父类的属性和方法,并且还可以在此基础上进行扩展或重写。继承的概念提供了代码重用和层次化设计的能力。
3.1 继承的语法
在 Java 中,使用 extends 关键字来实现类的继承。子类可以继承父类的非私有属性和方法,包括实例变量、静态变量、实例方法和静态方法。继承的语法如下:
public class Subclass extends Superclass {
// 子类的成员变量和方法
}
在上述代码中,Subclass 是子类的名称,Superclass 是父类的名称。子类可以通过继承获得父类的属性和方法,同时可以在子类中定义自己的新属性和方法。
3.2 继承的特性
继承具有以下特性:
- 代码重用:通过继承,子类可以重用父类的代码,避免重复编写相同的代码。
- 扩展性:子类可以在继承的基础上添加新的属性和方法,以满足特定的需求。
- 方法重写:子类可以重写父类的方法,以实现自己的逻辑。通过方法重写,子类可以改变父类方法的行为。
- 继承链:Java 支持多层继承,即一个类可以继承另一个类的子类,形成继承的链式结构。
- 访问控制:继承关系中,子类可以访问父类的公共和受保护的成员,但不能访问私有成员。
4.多态
多态是面向对象编程中的一个重要概念,它允许使用父类类型的引用变量来引用子类对象,从而实现代码的灵活性和可扩展性。多态性可以通过方法重写和继承关系来实现。
public class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
public void sound() {
System.out.println("Dog wang");
}
}
public class Cat extends Animal {
public void sound() {
System.out.println("Cat miao");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.sound(); // 输出:Dog wang
animal2.sound(); // 输出:Cat miao
}
}
在上述代码中,定义了一个父类 Animal 和两个子类 Dog 和 Cat。父类 Animal 中定义了 sound() 方法,子类 Dog 和 Cat 分别重写了该方法。在 Main 类中,通过父类变量分别引用 Dog 和 Cat 对象,并调用 sound() 方法。由于多态性的存在,根据对象的实际类型,调用相应子类的方法。
4.1 多态的使用前提
要使用多态性,需要满足以下三个条件:
-
继承关系:存在父类和子类之间的继承关系,子类继承了父类的属性和方法。
-
方法重写:子类需要重写父类的方法,以实现自己的特定行为。方法的签名(名称、参数列表)必须与父类中被重写的方法相同。
-
父类引用指向子类对象:将子类对象赋值给父类类型的引用变量,即向上转型。
4.2 多态访问成员的特点
-
编译时类型与运行时类型不一致:编译时,变量被声明为父类类型,而运行时,实际引用的是子类对象。
-
成员访问以运行时类型为准:在多态情况下,成员变量的访问以编译时类型为准,而方法的访问以运行时类型为准。
-
变量和静态成员:编译看左,运行看左
-
方法:编译看左,运行看右
-
-
父类引用指向子类对象:可以使用父类类型的引用变量来引用子类对象。
-
方法重写实现动态绑定:多态性的关键是方法的重写。在运行时,根据对象的实际类型来确定调用哪个类的方法。
4.3 向上转型和向下转型
-
向上转型(Upcasting):向上转型是指将子类对象赋值给父类类型的引用变量。向上转型可以隐式地进行,不需要显式的类型转换操作。
-
向下转型(Downcasting):向下转型是指将父类类型的引用变量转换为子类类型的引用变量。向下转型需要显式地进行,并且需要确保转换是安全的,即父类引用变量引用的对象实际上是子类对象。
向下转型的语法格式:
SubClass subObj = (SubClass) superObj;向下转型的注意事项:
- 转换过程中,需要确保父类引用变量引用的对象实际上是子类对象,否则会抛出
ClassCastException异常。 - 在进行向下转型之前,最好使用
instanceof运算符进行类型检查,确保转换是安全的。
向下转型的作用是恢复被向上转型的对象的真实类型,以便访问子类特有的成员变量和方法。
- 转换过程中,需要确保父类引用变量引用的对象实际上是子类对象,否则会抛出
4.4 方法重载和方法重写的多态性
多态性可以通过方法重载和方法重写来实现。
-
方法重载(Method Overloading):方法重载是在同一个类中定义多个同名方法,但参数列表不同。在编译时根据参数类型和个数确定调用哪个方法。
-
方法重写(Method Overriding):方法重写是子类重写父类中的方法,子类方法与父类方法具有相同的名称、返回类型和参数列表。在运行时根据实际对象类型确定调用哪个方法。
4.5 抽象类和接口的多态性
抽象类和接口也可以实现多态性。
-
抽象类(Abstract Class):抽象类是不能被实例化的,它只能作为其他类的父类来使用。通过抽象类可以定义抽象方法,子类必须实现这些抽象方法。抽象类的引用变量可以指向实际子类的对象。
-
接口(Interface):接口定义了一组方法的规范,它是一种纯粹的抽象类,只包含抽象方法和常量的声明。实现接口的类必须实现接口中声明的所有方法。接口的引用变量可以指向实现了该接口的类的对象。
通过抽象类和接口的多态性,可以实现代码的灵活性和可扩展性。
4.6 多态的应用场景
多态性在实际的软件开发中有广泛的应用,其中一些常见的应用场景包括:
-
代码复用:通过多态性可以编写通用的、可复用的代码,提高代码的可维护性和可读性。
-
扩展性:多态性可以方便地扩展程序功能,通过添加新的子类来实现新的功能,而不需要修改已有的代码。
-
运行时动态绑定:多态性实现了运行时动态绑定,根据实际对象类型来确定调用哪个方法,提供了更灵活的程序行为。
-
接口实现:多态性常用于接口的实现,通过接口引用变量来调用实现了该接口的类的方法。
5. 抽象类
抽象类是一种特殊的类,它不能被实例化,只能被继承。抽象类可以包含普通方法和抽象方法,其中抽象方法是没有实现的方法,需要在子类中进行具体实现。
5.1 抽象类的定义和声明
在Java中,使用abstract关键字来定义抽象类。抽象类可以拥有普通方法和抽象方法。抽象类的定义格式如下:
public abstract class ClassName {
// 抽象类的成员变量和方法
}
5.2 抽象方法
抽象方法是没有具体实现的方法,只有方法的声明,没有方法体。抽象方法用于定义子类必须实现的方法。
在抽象类中,使用abstract关键字来定义抽象方法。抽象方法的声明格式如下:
public abstract void methodName();
5.3 抽象类的特点
- 抽象类不能被实例化,只能作为其他类的父类来使用。
- 抽象类可以包含普通方法和抽象方法。
- 如果一个类继承了抽象类,那么它必须实现抽象类中的所有抽象方法,除非它自身也声明为抽象类。
- 抽象类可以拥有成员变量,构造方法和非抽象方法的实现。
- 抽象类的子类可以继续被其他类继承。
5.4 抽象类的应用
抽象类主要用于定义一组相关的类的行为和规范。它常用于以下场景:
- 定义一组类的共同特征和行为,抽象类作为这组类的父类,可以提供一些通用的方法和属性。
- 定义接口的实现类,抽象类可以实现接口中的一部分方法,让子类只需实现剩余的方法。
- 作为框架设计的基础,抽象类提供了一些基本的功能和约束,具体实现由子类完成。
6. 接口
接口是一种完全抽象的类,它只包含常量和抽象方法的声明,没有具体实现。接口定义了一组方法的规范,实现接口的类必须实现接口中声明的所有方法。
6.1 接口的定义和声明
在Java中,使用interface关键字来定义接口。接口中只包含常量和抽象方法的声明,没有具体实现。接口的定义格式如下:
public interface InterfaceName {
// 接口的常量声明
// 接口的抽象方法声明
}
6.2 接口的特点
- 接口中的方法都是抽象方法,没有具体实现。
- 接口中的常量默认为
public static final类型的,可以直接通过接口名访问。 - 类可以实现多个接口,实现接口的类必须实现接口中声明的所有方法。
- 接口可以继承其他接口,使用
extends关键字。 - 标记接口:clonable,serializable
6.3 接口的应用
接口主要用于定义一组类的行为和规范,它具有以下应用场景:
- 定义类的规范和行为,强制实现类遵循接口中声明的方法。
- 多态性的实现,通过接口类型引用对象,实现不同类的对象的统一调用。
- 定义回调方法,类实现接口并实现接口中的回调方法,供其他类调用。
接口在Java中广泛应用,是实现类与实现细节的解耦的重要手段之一。
7. 常用类
本节介绍一些常用的Java类,包括Object、Scanner、String、StringBuffer、Arrays、包装类和Random。
7.1 Object
java.lang.Object 是所有类的父类,在Java中所有的类都直接或间接继承自Object类。
Object类提供了一些常用的方法:
-
equals(): 比较对象的内容,可以根据自定义规则进行重写,用于判断对象是否相等。底层使用“==”,会比较对象的地址值。
-
hashCode():返回对象的hash值。 -
toString():以字符的形式打印对象的值,直接打印对象名便是调用此方法。 -
getClass:获取当前类型。 -
finalize():手动垃圾回收。 -
clone():浅拷贝。
7.2 Scanner
java.util.Scanner 类用于从标准输入、文件或字符串等来源读取输入数据。它提供了一系列的方法,如nextInt()、nextDouble()、nextLine()(可接收特殊字符)等,用于读取不同类型的输入数据。
7.3 String
java.lang.String 类代表字符串。它是不可变的,一旦创建就不能被修改。
String类提供了许多用于操作字符串的方法:
- 长度获取:
length()方法用于获取字符串的长度。 - 字符获取:
charAt(int index)方法用于获取指定索引位置的字符。 - 字符串拼接:
concat(String str)方法用于将指定字符串连接到原字符串的末尾。 - 子字符串提取:
substring(int beginIndex, int endIndex)方法用于获取原字符串的子字符串。 - 字符串分割:
split(String regex)方法用于将字符串按照指定的分隔符进行分割,并返回一个字符串数组。 - 字符串替换:
replace(char oldChar, char newChar)方法用于将原字符串中的指定字符替换为新的字符。 - 字符串查找:
indexOf(String str)方法用于在原字符串中查找指定字符串,并返回第一次出现的索引位置。
7.4 StringBuffer
java.lang.StringBuffer 类用于可变字符串的操作。与String不同,StringBuffer对象的内容可以被修改。StringBuffer类提供了许多方法,用于在字符串中执行插入、追加、删除等操作。
-
构造方法
StringBuffer(): 创建一个空的StringBuffer对象。StringBuffer(int capacity): 创建一个指定容量的StringBuffer对象。StringBuffer(String str): 创建一个包含指定字符串的StringBuffer对象。
-
常用方法
append(String str): 在字符串末尾追加指定字符串。insert(int offset, String str): 在指定位置插入指定字符串。delete(int start, int end): 删除指定范围内的字符。reverse(): 反转字符串。replace(int start, int end, String str): 用指定字符串替换指定范围内的字符。length(): 返回字符串的长度。capacity(): 返回当前容量(可容纳的字符数)。toString(): 将StringBuffer对象转换为字符串。
示例代码:
// 创建一个空的StringBuffer对象
StringBuffer sb1 = new StringBuffer();
// 创建一个指定容量的StringBuffer对象
StringBuffer sb2 = new StringBuffer(16);
// 创建一个包含指定字符串的StringBuffer对象
StringBuffer sb3 = new StringBuffer("Hello");
// 在字符串末尾追加指定字符串
sb3.append(" World");
// 在指定位置插入指定字符串
sb3.insert(5, " Java");
// 删除指定范围内的字符
sb3.delete(5, 10);
// 反转字符串
sb3.reverse();
// 用指定字符串替换指定范围内的字符
sb3.replace(0, 5, "Hi");
// 获取字符串的长度
int length = sb3.length();
// 获取当前容量
int capacity = sb3.capacity();
// 将StringBuffer对象转换为字符串
String str = sb3.toString();
StringBuffer类的方法允许我们动态地修改字符串内容,可以用于频繁的字符串操作,如拼接、插入、删除和替换等。由于StringBuffer是可变的,它的性能比String更高效。
StringBuffer 是线程安全的类,需要注意的是,由于线程安全的机制,StringBuffer 在某些情况下的性能可能会受到影响。如果不需要线程安全的操作,可以使用非线程安全的 StringBuilder 类,它提供了类似的可变字符串操作,但没有同步开销。
7.5 Arrays
java.util.Arrays 类提供了用于操作数组的各种方法,如排序、搜索、比较和填充等。
-
排序方法
sort(array): 对数组进行升序排序。sort(array, fromIndex, toIndex): 对数组指定范围内的元素进行升序排序。parallelSort(array): 使用并行算法对数组进行升序排序。parallelSort(array, fromIndex, toIndex): 使用并行算法对数组指定范围内的元素进行升序排序。
-
搜索方法
binarySearch(array, key): 使用二分查找算法在排序后的数组中搜索指定元素。binarySearch(array, fromIndex, toIndex, key): 使用二分查找算法在排序后的数组指定范围内搜索指定元素。
-
比较方法
equals(array1, array2): 比较两个数组是否相等。deepEquals(array1, array2): 深度比较两个数组是否相等,会递归比较多维数组的元素。
-
填充方法
fill(array, value): 将数组的所有元素都设置为指定的值。fill(array, fromIndex, toIndex, value): 将数组指定范围内的元素都设置为指定的值。
-
转换方法
toString(array): 将数组转换为字符串形式。deepToString(array): 将多维数组转换为字符串形式。
-
数组操作方法
copyOf(array, length): 复制指定长度的数组。copyOfRange(array, fromIndex, toIndex): 复制指定范围内的数组元素。asList(array): 将数组转换为List集合。
示例代码:
int[] numbers = {5, 2, 8, 3, 1};
// 对数组进行升序排序
Arrays.sort(numbers);
// 在排序后的数组中搜索指定元素
int index = Arrays.binarySearch(numbers, 3);
// 比较两个数组是否相等
boolean isEqual = Arrays.equals(numbers, new int[]{1, 2, 3, 5, 8});
// 将数组的所有元素设置为指定的值
Arrays.fill(numbers, 0);
// 将数组转换为字符串形式
String str = Arrays.toString(numbers);
// 复制指定长度的数组
int[] copy = Arrays.copyOf(numbers, 3);
// 将数组转换为List集合
List<Integer> list = Arrays.asList(numbers);
Arrays类提供了丰富的方法来简化数组的操作。这些方法能够提高数组的排序、搜索、比较和填充等操作的效率和便利性。
7.6 包装类
Java提供了与基本数据类型对应的包装类,用于将基本数据类型转换为对象。这些包装类包括Boolean、Character、Byte、Short、Integer、Long、Float和Double。包装类提供了一些常用的方法,如类型转换、比较大小等。
7.7 Random
java.util.Random 类用于生成伪随机数。它提供了一系列的方法,如nextInt()、nextDouble()、nextBoolean()等,用于生成不同类型的随机数。
第三章:异常处理
异常处理是Java编程中重要的概念,它涉及到如何识别、捕获和处理程序运行过程中出现的错误情况。本章将介绍异常的概念、分类和处理机制,以及常见的异常类和一些异常处理的技巧和注意事项。
1. 异常和Throwable 接口
异常是指程序在运行过程中遇到的意外情况或错误,它会导致程序的正常执行流程被中断。异常通常包含有关错误类型和错误发生位置的信息,帮助程序员定位和解决问题。
Throwable 接口是 Java 异常处理机制的根接口,其包含了处理异常时所需的基本方法和属性。所有的异常都实现了 Throwable 接口,包括错误(Error)和异常(Exception)。下面是 Throwable 接口的方法和说明。
public String getMessage()
返回此 Throwable 对象的详细消息字符串。如果此 Throwable 的详细消息字符串不存在,则返回 null。
public String getLocalizedMessage()
返回此 Throwable 对象的本地化详细消息字符串。如果子类没有覆盖此方法,则将返回与 getMessage() 相同的字符串。
public synchronized Throwable getCause()
返回此 Throwable 对象的原因或 null(如果原因不存在或未知)。在创建原因时,还可以提供异常处理的详细信息。
public String toString()
返回此 Throwable 对象的字符串表示形式,包括它的类名称、详细消息以及它的原因(如果存在)。
public void printStackTrace()
将此 Throwable 及其追踪输出到标准错误流。此方法生成的输出类似于此 Throwable 对象的信息,以及从导致方法调用的位置开始的每个方法调用的类、方法名和行号。
public StackTraceElement[] getStackTrace()
返回此 Throwable 对象的追踪,以数组形式返回。该方法返回一个包含了方法调用栈的数组,其中最后一个元素表示最先调用的方法,第一个元素表示最后调用的方法。该方法返回的数组是拷贝,所以对该数组的任何更改都不会影响到此 Throwable 对象的追踪信息。
public void setStackTrace(StackTraceElement[] stackTrace)
将指定的 StackTraceElement 数组设置为此 Throwable 对象的堆栈跟踪。此方法不允许修改此 Throwable 对象的原因或详细消息。此方法允许在原因不存在或未知的情况下初始化堆栈跟踪,或者覆盖堆栈跟踪。
2. 异常的分类
异常按照其发生的时机和性质可以分为三种类型:
- 受检异常(
Checked Exception):在编译阶段就需要进行处理的异常,必须在代码中使用try-catch块或throws关键字进行处理。常见的受检异常有IOException、ClassNotFoundException等。 - 运行时异常(
Runtime Exception):在程序运行过程中可能出现的异常,不要求强制处理,可以选择性地进行处理。常见的运行时异常有ArithmeticException、NullPointerException等。 - 错误(
Error):指Java虚拟机无法解决的严重问题,通常是由系统资源耗尽或不可恢复的错误导致的。不需要对错误进行处理,程序无法从错误中恢复。
3. 异常处理的机制
异常处理机制主要包括以下几个部分:
-
try-catch块用于捕获和处理异常。try块中包含可能抛出异常的代码,而catch块用于捕获并处理异常。catch块可以指定捕获特定类型的异常,并提供相应的处理逻辑。 -
finally块用于定义无论是否发生异常都需要执行的代码。无论try块中是否发生异常,finally块中的代码都会被执行。通常在finally块中进行资源释放或清理操作。 -
throw语句用于手动抛出异常。通过throw语句可以在程序中显式地抛出指定的异常,以便由上层代码进行捕获和处理。 -
throws关键字用于在方法声明中指定可能抛出的异常类型。当方法中可能抛出受检异常时,需要使用throws关键字声明异常类型,以便调用该方法的代码进行处理。- 使用
try-catch块捕获和处理异常:
try { // 可能抛出异常的代码 int result = divide(10, 0); System.out.println("结果:" + result); } catch (ArithmeticException e) { // 捕获 ArithmeticException 异常并处理 System.out.println("除数不能为零!"); }- 使用
finally块确保资源的释放:
FileWriter fileWriter = null; try { fileWriter = new FileWriter("file.txt"); // 写入文件的代码 fileWriter.write("Hello, World!"); } catch (IOException e) { // 处理 IO 异常 e.printStackTrace(); } finally { // 无论是否发生异常,确保关闭资源 if (fileWriter != null) { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } }- 使用
throw语句抛出异常,使用throws关键字声明方法可能抛出的异常:
public void depositMoney(double amount) throws InsufficientFundsException { if (amount <= 0) { throw new IllegalArgumentException("存款金额必须大于零"); } if (amount > balance) { throw new InsufficientFundsException("余额不足"); } // 其他存款操作 } - 使用
4. 自定义异常
Java允许用户自定义异常,以满足特定的业务需求。自定义异常需要继承自Exception或其子类,并根据需要添加额外的字段和方法。
5. 常见的异常类
Java提供了许多常见的异常类,用于表示不同类型的错误和异常情况。以下是几个常见的异常类及其说明:
ArithmeticException是运行时异常,表示在数学运算过程中发生了算术错误,例如除以零。NullPointerException是运行时异常,表示尝试访问空对象的成员或调用空对象的方法。ArrayIndexOutOfBoundsException是运行时异常,表示数组访问越界,即尝试访问不存在的数组元素。IllegalArgumentException是运行时异常,表示方法接收到非法的参数。ClassNotFoundException是受检异常,表示尝试加载不存在的类。IOException是受检异常,表示输入输出操作中发生了错误,例如文件读写失败。Exception是Java中所有异常类的基类,它是受检异常的直接或间接父类。
6. 多重捕获和异常链
Java允许在一个try-catch块中捕获多个异常,并根据不同的异常类型执行相应的处理逻辑。可以使用多个catch块来处理不同类型的异常。
异常链是指在异常处理过程中,一个异常引发了另一个异常。可以使用异常的构造方法将原始异常作为参数传递给新的异常,并将其添加到异常链中,以便更好地追踪和定位问题。
7. 异常处理和注意事项
try代码块中捕获到异常后,程序将跳转到匹配的catch块并执行相应的处理代码。在catch块中处理异常后,程序将继续执行catch块之后的代码,而不会继续执行try块中异常抛出点之后的代码。- 使用
throw关键字抛出异常后,程序将立即跳出当前的执行流程,并开始查找匹配的异常处理代码。如果当前的方法没有捕获到该异常,异常将被传递到调用该方法的上层方法,直到找到相应的异常处理代码。 - 在使用多个
catch块处理不同类型的异常时,应该将子类异常的catch块放在父类异常的catch块之前。只有第一个匹配的catch块会被执行,而后续的catch块将被忽略。 - 如果在方法签名中使用了
throws关键字声明方法可能抛出的异常,调用该方法时,调用方必须要么捕获这些异常,要么继续将这些异常向上层调用传递。 - 在异常处理过程中,应根据实际情况选择合适的异常处理策略,例如记录日志、回滚事务、重新尝试操作等。
第四章:集合
集合(Collections)是Java中常用的数据结构,用于存储和操作一组相关的数据元素。集合框架提供了一套强大而灵活的类和接口,用于处理各种类型的集合数据。本章将介绍Java集合框架的概念、常见的集合类和接口,以及集合的常用操作和技巧。
1. 集合框架概述
集合框架(Collections Framework)是Java中用于存储和操作数据的一组类和接口。它提供了一套统一的编程接口,用于管理和操作不同类型的集合数据。集合框架的设计目标是提供高效、可靠和可扩展的集合实现,使开发人员能够更轻松地处理和操作数据。
1.1 集合的作用和优势
集合框架在Java编程中具有重要的作用和优势:
- 数据存储和组织:集合框架提供了多种集合类和接口,可以方便地存储和组织数据,例如列表、集合、映射等。
- 高性能和效率:集合框架的实现经过优化,可以提供高性能的数据访问和操作。
- 类型安全:通过使用泛型(Generic)技术,集合框架提供了类型安全的数据存储和操作,减少了类型转换的错误和麻烦。
- 代码重用:集合框架提供了一套通用的接口和算法,可以方便地重用代码,减少了开发时间和工作量。
- 可扩展性:集合框架的设计允许开发人员根据需要自定义和扩展集合类,以满足特定的业务需求。
1.2 集合框架的体系结构
集合框架的体系结构是一个层次化结构,由一组接口和类组成,用于存储和操作不同类型的集合数据。下面是集合框架的体系结构概述:
-
根接口:
java.util.CollectionCollection是集合框架的根接口,它定义了一组通用的方法,用于操作集合中的元素。Collection接口的常见实现类包括List、Set、Queue和Map。
-
列表接口:
java.util.ListList接口是一个有序的集合,可以包含重复元素。List接口定义了有关元素的索引、插入、删除和访问的操作。List接口的常见实现类包括ArrayList、LinkedList和Vector等。
-
集合接口:
java.util.SetSet接口是一个不允许包含重复元素的集合。Set接口定义了添加、删除和检查元素是否存在的操作。Set接口的常见实现类包括HashSet、TreeSet和LinkedHashSet等。
-
队列接口:
java.util.QueueQueue接口是一个用于操作队列数据结构的接口。Queue接口定义了添加、删除和检查队列中的元素的操作。Queue接口的常见实现类包括LinkedList和PriorityQueue等。
-
映射接口:
java.util.MapMap接口是一种键值对(key-value)映射的集合。Map接口定义了添加、删除和检查键值对的操作。Map接口的常见实现类包括HashMap、TreeMap和LinkedHashMap等。
集合框架的体系结构中,根接口Collection提供了最基本的集合操作,而列表接口List、集合接口Set、队列接口Queue和映射接口Map则分别针对不同的需求提供了特定的操作和功能。每个接口都有对应的实现类,开发人员可以根据具体需求选择适合的集合类来存储和操作数据。
此外,集合框架还包括一些抽象类和工具类,用于提供通用的实现和算法。例如,抽象类AbstractCollection和AbstractList提供了部分集合功能的实现细节,而工具类Collections提供了集合操作的工具方法,如排序、搜索等。
2. 泛型与集合
2.1 泛型的概念和作用
-
泛型(Generics)是Java引入的一种类型参数化机制,用于增强代码的类型安全性和重用性。
-
泛型的作用:
- 提供编译时的类型检查,避免在运行时出现类型错误。
- 使代码更加通用和灵活,可适用于不同类型的数据。
- 提供编译时的类型推断,减少类型转换的繁琐和风险。
-
示例代码:
// 声明一个泛型类 class Box<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } // 使用泛型类 Box<String> stringBox = new Box<>(); stringBox.setItem("Hello"); String str = stringBox.getItem();
2.2 泛型集合类和接口
-
Java提供了许多泛型集合类和接口,用于存储和操作各种类型的数据。
-
常见的泛型集合类和接口包括:
ArrayList<E>: 动态数组,可变长度的列表。LinkedList<E>: 双向链表,适用于频繁的插入和删除操作。HashSet<E>: 基于哈希表实现的无序集合,不允许重复元素。TreeSet<E>: 基于红黑树实现的有序集合,不允许重复元素。HashMap<K, V>: 基于哈希表实现的键值对映射。TreeMap<K, V>: 基于红黑树实现的有序键值对映射。
-
示例代码:
List<String> list = new ArrayList<>(); list.add("Apple"); list.add("Banana"); Set<Integer> set = new HashSet<>(); set.add(1); set.add(2); Map<String, Integer> map = new HashMap<>(); map.put("Apple", 1); map.put("Banana", 2);
2.3 使用泛型提高类型安全性
-
泛型可以提高代码的类型安全性,避免在编译时和运行时出现类型错误。
-
使用泛型的好处:
- 编译器可以进行类型检查,确保只有兼容的类型可以传递给泛型类或方法。
- 避免了在运行时进行类型转换,减少了出现类型转换异常的风险。
- 提供了更好的代码可读性和可维护性,明确了代码中的数据类型。
-
示例代码:
// 泛型类 class Box<T> { private T item; public void setItem(T item) { this.item = item; } public T getItem() { return item; } } // 泛型方法 public <E> void printList(List<E> list) { for (E item : list) { System.out.println(item); } } // 使用泛型类和泛型方法 Box<String> stringBox = new Box<>(); stringBox.setItem("Hello"); String str = stringBox.getItem(); List<Integer> intList = new ArrayList<>(); intList.add(1); intList.add(2); printList(intList);
3. 集合接口的成员方法
3.1 Collection 接口的成员方法
boolean add(E element): 将指定的元素添加到集合中。boolean addAll(Collection<? extends E> collection): 将指定集合中的所有元素添加到当前集合中。void clear(): 清空集合中的所有元素。boolean contains(Object object): 判断集合中是否包含指定的元素。boolean containsAll(Collection<?> collection): 判断集合是否包含指定集合中的所有元素。boolean isEmpty(): 判断集合是否为空。Iterator<E> iterator(): 返回在集合上进行迭代的迭代器。boolean remove(Object object): 从集合中移除指定的元素。boolean removeAll(Collection<?> collection): 从集合中移除包含在指定集合中的所有元素。boolean retainAll(Collection<?> collection): 保留集合中与指定集合相同的元素,移除其他元素。int size(): 返回集合中的元素个数。Object[] toArray(): 将集合转换为对象数组。<T> T[] toArray(T[] array): 将集合转换为指定类型的数组。
3.2 List 接口的成员方法
除了继承自 Collection 接口的方法外,List 接口还定义了以下方法:
void add(int index, E element): 在指定的索引位置插入元素。boolean addAll(int index, Collection<? extends E> collection): 在指定的索引位置插入指定集合中的所有元素。E get(int index): 返回指定索引位置的元素。int indexOf(Object object): 返回元素第一次出现的索引,若不存在返回 -1。int lastIndexOf(Object object): 返回元素最后一次出现的索引,若不存在返回 -1。ListIterator<E> listIterator(): 返回列表元素的双向迭代器。ListIterator<E> listIterator(int index): 返回从指定索引开始的列表元素的双向迭代器。E remove(int index): 移除指定索引位置的元素。E set(int index, E element): 将指定索引位置的元素替换为新元素。List<E> subList(int fromIndex, int toIndex): 返回指定索引范围内的子列表。
3.3 Set 接口的成员方法
除了继承自 Collection 接口的方法外,Set 接口还定义了以下方法:
boolean add(E element): 将指定的元素添加到集合中。boolean addAll(Collection<? extends E> collection): 将指定集合中的所有元素添加到当前集合中。boolean contains(Object object): 判断集合中是否包含指定的元素。boolean containsAll(Collection<?> collection): 判断集合是否包含指定集合中的所有元素。boolean remove(Object object): 从集合中移除指定的元素。boolean removeAll(Collection<?> collection): 从集合中移除包含在指定集合中的所有元素。boolean retainAll(Collection<?> collection): 保留集合中与指定集合相同的元素,移除其他元素。int size(): 返回集合中的元素个数。void clear(): 清空集合中的所有元素。boolean isEmpty(): 判断集合是否为空。Iterator<E> iterator(): 返回在集合上进行迭代的迭代器。
3.4 Queue 接口的成员方法
除了继承自 Collection 接口的方法外,Queue 接口还定义了以下方法:
boolean add(E element): 将指定的元素添加到队列中。boolean offer(E element): 将指定的元素添加到队列中。E remove(): 移除并返回队列头部的元素。E poll(): 移除并返回队列头部的元素,若队列为空则返回 null。E element(): 返回队列头部的元素,但不移除。E peek(): 返回队列头部的元素,若队列为空则返回 null。
3.5 Map 接口的成员方法
void clear(): 清空映射中的所有键值对。boolean containsKey(Object key): 判断映射中是否包含指定的键。boolean containsValue(Object value): 判断映射中是否包含指定的值。Set<Map.Entry<K, V>> entrySet(): 返回包含映射中所有键值对的 Set 视图。V get(Object key): 返回与指定键关联的值,若键不存在则返回 null。boolean isEmpty(): 判断映射是否为空。Set<K> keySet(): 返回包含映射中所有键的 Set 视图。V put(K key, V value): 将指定的键值对添加到映射中,若键已存在则替换值并返回旧值。void putAll(Map<? extends K, ? extends V> map): 将指定映射中的所有键值对添加到当前映射中。V remove(Object key): 移除并返回与指定键关联的值,若键不存在则返回 null。int size(): 返回映射中的键值对数量。Collection<V> values(): 返回包含映射中所有值的 Collection 视图。
4. 常用的集合类
在Java的集合框架中,有多种常用的集合类可供选择,每种集合类都有其特定的用途和适用场景。下面介绍了常用的集合类及其特点。
4.1 ArrayList
ArrayList是基于数组实现的动态数组,它可以根据需要自动扩容。- 特点:
- 可以快速访问指定位置的元素,时间复杂度为O(1)。
- 在末尾进行插入和删除操作的性能较好,时间复杂度为O(1)。
- 在中间插入和删除元素时需要移动其他元素,性能较差,时间复杂度为O(n)。
- 不适合频繁的插入和删除操作。
- 示例代码:
List<String> arrayList = new ArrayList<>(); arrayList.add("Apple"); arrayList.add("Banana"); arrayList.add("Orange");
4.2 LinkedList
LinkedList是基于链表实现的双向链表,它可以高效地进行插入和删除操作。- 特点:
- 在任意位置进行插入和删除操作的性能较好,时间复杂度为O(1)。
- 随机访问元素时需要遍历链表,性能较差,时间复杂度为O(n)。
- 适合频繁的插入和删除操作。
- 示例代码:
List<String> linkedList = new LinkedList<>(); linkedList.add("Apple"); linkedList.add("Banana"); linkedList.add("Orange");
4.3 HashSet
HashSet是基于哈希表实现的无序集合,它使用哈希函数来存储和查找元素。- 特点:
- 元素无序,不重复。
- 添加、删除和查找操作的性能较好,平均时间复杂度为O(1)。
- 不保证元素的顺序,不适用于有序操作。
- 示例代码:
Set<String> hashSet = new HashSet<>(); hashSet.add("Apple"); hashSet.add("Banana"); hashSet.add("Orange");
4.4 TreeSet
TreeSet是基于红黑树(平衡二叉树)实现的有序集合,它可以自动按照元素的自然顺序进行排序。- 特点:
- 元素有序,不重复。
- 添加、删除和查找操作的性能较好,平均时间复杂度为O(log n)。
- 支持自定义排序规则。
- 示例代码:
Set<String> treeSet = new TreeSet<>(); treeSet.add("Apple"); treeSet.add("Banana"); treeSet.add("Orange");
4.5 HashMap
-
HashMap是基于哈希表实现的键值对映射,它使用哈希函数来存储和查找键值对。- 特点:
- 键值对无序,键不重复。
- 添加、删除和查找操作的性能较好,平均时间复杂度为O(1)。
- 允许使用null作为键和值。
- 示例代码:
Map<String, Integer> hashMap = new HashMap<>(); hashMap.put("Apple", 1); hashMap.put("Banana", 2); hashMap.put("Orange", 3);
- 特点:
4.6 Hashtable
-
Hashtable是基于哈希表实现的键值对映射,类似于HashMap,但它是线程安全的。 -
特点:
- 键值对无序,键不重复。
- 添加、删除和查找操作的性能较好,平均时间复杂度为O(1)。
- 线程安全,适用于多线程环境。
-
示例代码:
Map<String, Integer> hashtable = new Hashtable<>(); hashtable.put("Apple", 1); hashtable.put("Banana", 2); hashtable.put("Orange", 3);
4.7 TreeMap
TreeMap是基于红黑树(平衡二叉树)实现的有序键值对映射,它可以自动按照键的自然顺序进行排序。- 特点:
- 键值对有序,键不重复。
- 添加、删除和查找操作的性能较好,平均时间复杂度为O(log n)。
- 支持自定义键的排序规则。
- 示例代码:
Map<String, Integer> treeMap = new TreeMap<>(); treeMap.put("Apple", 1); treeMap.put("Banana", 2); treeMap.put("Orange", 3);
5. 集合的常用操作
5.1 遍历集合
- 遍历集合是指逐个访问集合中的元素。
- 常见的遍历方式包括使用迭代器(Iterator)、增强型for循环和Java 8引入的流(Stream)。
- 示例代码:
List<String> list = new ArrayList<>(); list.add("Apple"); list.add("Banana"); // 使用迭代器遍历 Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); System.out.println(item); } // 使用增强型for循环遍历 for (String item : list) { System.out.println(item); } // 使用流遍历 list.stream().forEach(System.out::println);
5.2 集合的排序
- 对集合进行排序是指将集合中的元素按照特定的排序规则进行排序。
- 集合的排序通常使用
Collections.sort()方法(对List集合进行排序)或Arrays.sort()方法(对数组进行排序)。 - 需要注意的是,被排序的元素必须实现
Comparable接口,或者提供自定义的比较器(Comparator)。 - 示例代码:
List<Integer> numbers = new ArrayList<>(); numbers.add(5); numbers.add(2); numbers.add(8); // 对List集合进行自然排序 Collections.sort(numbers); System.out.println(numbers); // 输出:[2, 5, 8] // 对List集合使用自定义的比较器进行排序 Collections.sort(numbers, new CustomComparator()); System.out.println(numbers); // 输出:[8, 5, 2]
5.3 集合的查找和替换
- 集合的查找和替换操作是指在集合中查找指定的元素或者替换集合中的某个元素。
- 常见的查找和替换方法包括使用
contains()方法判断元素是否存在,使用indexOf()方法查找元素的位置,使用get()方法获取指定位置的元素,使用set()方法替换指定位置的元素等。 - 示例代码:
List<String> fruits = new ArrayList<>(); fruits.add("Apple"); fruits.add("Banana"); fruits.add("Orange"); // 判断集合中是否包含指定元素 boolean containsApple = fruits.contains("Apple"); System.out.println(containsApple); // 输出:true // 查找元素的位置 int indexOfBanana = fruits.indexOf("Banana"); System.out.println(indexOfBanana); // 输出:1 // 获取指定位置的元素 String fruit = fruits.get(2); System.out.println(fruit); // 输出:Orange // 替换指定位置的元素 fruits.set(0, "Mango"); System.out.println(fruits); // 输出:[Mango, Banana, Orange]
5.4 集合的过滤和筛选
集合的过滤和筛选是指根据特定的条件从集合中选择符合条件的元素或者剔除不符合条件的元素。
可以使用循环遍历集合的方式,结合条件判断语句,筛选出符合条件的元素。示例代码如下:
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
evenNumbers.add(number);
}
}
System.out.println(evenNumbers); // 输出:[2, 4]
5.5 集合的转换和拷贝
集合的转换和拷贝操作是指将一个集合转换为另一个类型的集合,或者将一个集合的元素拷贝到另一个集合中。
- 使用构造方法进行集合转换和拷贝
可以使用集合类的构造方法来进行集合的转换和拷贝操作,通过传入另一个集合作为参数,将一个集合转换为另一个类型的集合,或者将一个集合的元素拷贝到另一个集合中。示例代码如下:
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
Set<Integer> numberSet = new HashSet<>(numbers); // 将List转换为Set
List<Integer> copiedList = new ArrayList<>(numbers); // 拷贝List集合
System.out.println(numberSet); // 输出:[1, 2, 3, 4, 5]
System.out.println(copiedList); // 输出:[1, 2, 3, 4, 5]
- 使用Collections类的方法进行集合转换和拷贝
Collections类提供了一些方法用于集合的转换和拷贝,如addAll()方法和addAll()方法可以将一个集合的元素添加到另一个集合中。示例代码如下:
List<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.add(4);
numbers.add(5);
List<Integer> newNumbers = new ArrayList<>();
// 将numbers集合中大于2的元素添加到newNumbers集合中
for (Integer number : numbers) {
if (number > 2) {
newNumbers.add(number);
}
}
System.out.println(newNumbers); // 输出:[3, 4, 5]
6.常见的集合面试题
-
集合框架的主要接口有哪些?它们之间的关系是什么?
-
ArrayList和LinkedList的区别是什么?在什么情况下应该使用它们? -
HashSet和TreeSet的区别是什么?它们是如何维护元素的唯一性和排序的? -
HashMap和HashTable的区别是什么?它们如何处理哈希冲突?在多线程环境中使用它们时需要注意什么? -
ConcurrentHashMap是如何实现线程安全的? -
如何遍历一个集合?有哪些不同的遍历方式?
-
如何对一个集合进行排序?有哪些不同的排序方法和接口可以使用?
-
如何实现自定义对象的比较和排序?
-
什么是
fail-fast机制?为什么会出现ConcurrentModificationException异常? -
什么是弱引用(
WeakReference)和软引用(SoftReference)?它们在集合中的应用有哪些? -
什么是并发集合类(
Concurrent Collections)?它们与普通集合类的区别是什么? -
如何实现线程安全的有序队列(
Priority Queue)? -
如何选择合适的集合类以及正确使用集合的注意事项?
-
集合框架的主要接口有哪些?它们之间的关系是什么?
答:主要接口包括Collection、List、Set、Queue、Map。List和Set都继承自Collection接口,Map则是独立的接口。List 是有序可重复的集合,Set是无序不可重复的集合,而Queue是一种特殊的集合,遵循先进先出(FIFO)的原则。Map是键值对的集合。 -
ArrayList 和 LinkedList 的区别是什么?在什么情况下应该使用它们?
答:ArrayList是基于数组实现的动态数组,支持随机访问和快速的插入/删除操作。LinkedList是基于链表实现的双向链表,适合频繁的插入/删除操作。如果需要随机访问元素或者对列表进行频繁的插入/删除操作,应使用ArrayList。如果需要在列表中进行频繁的插入/删除操作,应使用LinkedList。 -
HashSet和TreeSet的区别是什么?它们是如何维护元素的唯一性和排序的?
答:HashSet是基于哈希表实现的,不保证元素的顺序,使用哈希算法来维护元素的唯一性。TreeSet是基于红黑树实现的有序集合,根据元素的自然顺序或者自定义比较器来维护元素的排序和唯一性。 -
HashMap和HashTable的区别是什么?它们如何处理哈希冲突?在多线程环境中使用它们时需要注意什么?
答:HashMap和HashTable都是基于哈希表实现的键值对集合。它们的主要区别在于线程安全性和是否允许null键和值。HashMap是非线程安全的,允许使用null键和值;而HashTable是线程安全的,不允许使用null键和值。它们在处理哈希冲突时使用了不同的解决方法,HashMap使用链地址法(链表或红黑树),而HashTable使用开放地址法(线性探测)。 -
ConcurrentHashMap是如何实现线程安全的?
答:ConcurrentHashMap使用分段锁(Segment)来实现线程安全。它将整个集合分成多个段,每个段维护一部分键值对。在读取或修改键值对时,只需要锁定对应的段,而不需要锁定整个集合,从而提高并发性能。 -
如何遍历一个集合?有哪些不同的遍历方式?
答:遍历集合有多种方式,常见的遍历方式包括:
- 使用迭代器(Iterator):通过调用集合的
iterator()方法获取迭代器对象,然后使用hasNext()和next()方法进行遍历。这是一种通用的遍历方式,适用于所有实现了Iterable接口的集合类。
示例代码:
List<String> list = new ArrayList<>();
// 添加元素到集合...
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
// 处理元素...
}
- 使用增强型 for 循环:适用于遍历数组和实现了
Iterable接口的集合类。它简化了迭代器的使用,更加直观和简洁。
示例代码:
List<String> list = new ArrayList<>();
// 添加元素到集合...
for (String element : list) {
// 处理元素...
}
- 使用 Lambda 表达式和函数式接口(Java 8+):使用函数式接口
Consumer和forEach方法对集合进行遍历。这种方式更加简洁和灵活。
示例代码:
List<String> list = new ArrayList<>();
// 添加元素到集合...
list.forEach(element -> {
// 处理元素...
});
除了上述方式,还可以使用传统的 for 循环和索引访问集合元素。选择合适的遍历方式取决于具体的需求和代码风格。在遍历集合时,应注意避免在遍历过程中修改集合的结构,以免引发并发修改异常或导致意外的行为。
- 集合的排序如何实现?有哪些不同的排序方法?
答:集合的排序可以通过使用java.util.Collections类的sort()方法或使用集合自带的排序方法实现。常见的集合排序方法包括:
- 使用
Collections.sort()方法:对实现了List接口的集合进行排序。该方法会根据元素的自然顺序进行排序,或者通过自定义的比较器来指定排序规则。
示例代码:
List<Integer> list = new ArrayList<>();
// 添加元素到集合...
Collections.sort(list); // 默认按升序排序
- 使用集合自带的排序方法:某些集合类提供了自带的排序方法,如
TreeSet和TreeMap,它们会根据元素的自然顺序或者自定义的比较器进行排序。
示例代码:
Set<Integer> set = new TreeSet<>();
// 添加元素到集合...
- 自定义比较器:通过实现
java.util.Comparator接口,自定义比较器来指定排序规则。
示例代码:
List<Student> students = new ArrayList<>();
// 添加学生对象到集合...
Collections.sort(students, new StudentComparator()); // 使用自定义比较器进行排序
- 使用 Lambda 表达式和函数式接口(Java 8+):通过
Comparator函数式接口和sort()方法结合使用,可以更简洁地指定排序规则。
示例代码:
List<Student> students = new ArrayList<>();
// 添加学生对象到集合...
students.sort((s1, s2) -> s1.getName().compareTo(s2.getName())); // 按姓名排序
在进行集合排序时,要注意元素类型必须实现 Comparable 接口或者提供自定义的比较器。此外,排序会改变原始集合的顺序,因此在需要保留原始顺序的情况下,应先创建副本进行排序。
-
如何实现自定义对象的比较和排序?
-
使用自然顺序(
Comparable接口):-
让自定义类实现
Comparable接口,并重写compareTo()方法,定义对象之间的比较规则。 -
调用
Collections.sort()方法来对集合进行排序,该方法会使用自定义类的compareTo()方法进行比较。
-
示例代码:
import java.util.ArrayList; import java.util.Collections; import java.util.List; public class Person implements Comparable<Person> { private String name; private int age; // 构造方法、getter和setter等 @Override public int compareTo(Person other) { // 根据姓名比较 return this.name.compareTo(other.name); } } public class SortingExample { public static void main(String[] args) { List<Person> personList = new ArrayList<>(); personList.add(new Person("Alice", 25)); personList.add(new Person("Bob", 30)); personList.add(new Person("Charlie", 20)); Collections.sort(personList); for (Person person : personList) { System.out.println(person.getName() + ", " + person.getAge()); } } }上述示例中,
Person类实现了Comparable<Person>接口,并根据姓名进行比较。调用Collections.sort()方法对personList进行排序时,会使用Person类的compareTo()方法进行比较。-
使用自定义比较器(
Comparator接口):-
创建一个实现了
Comparator接口的类,并重写compare()方法,定义对象之间的比较规则。 -
调用
Collections.sort()方法并传入自定义比较器的实例,以指定比较规则。
-
示例代码:
import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Person { private String name; private int age; // 构造方法、getter和setter等 // 省略其他代码 public static class PersonAgeComparator implements Comparator<Person> { @Override public int compare(Person person1, Person person2) { // 根据年龄比较 return person1.getAge() - person2.getAge(); } } public static void main(String[] args) { List<Person> personList = new ArrayList<>(); personList.add(new Person("Alice", 25)); personList.add(new Person("Bob", 30)); personList.add(new Person("Charlie", 20)); Collections.sort(personList, new PersonAgeComparator()); for (Person person : personList) { System.out.println(person.getName() + ", " + person.getAge()); } } }在上述示例中,我们定义了一个
Person类,其中包含了姓名和年龄属性。然后,我们创建了一个内部类PersonAgeComparator,它实现了Comparator<Person>接口,并根据年龄进行比较。在
main()方法中,我们创建了一个personList的ArrayList对象,并添加了几个Person类的实例。然后,我们使用Collections.sort()方法对personList进行排序,传入了PersonAgeComparator的实例作为比较器。这样,personList中的对象将根据年龄属性进行排序。通过使用自定义比较器,我们可以根据特定的比较规则对集合中的自定义类进行排序,而无需修改自定义类本身的代码。
-
使用 lambda 表达式进行比较:
-
如果您使用的是 Java 8 或更高版本,您可以使用 lambda 表达式来简化比较器的创建和使用。
-
使用
Comparator.comparing()方法来创建一个比较器,指定要比较的属性或字段。 -
调用
Collections.sort()方法并传入比较器,以指定比较规则。
-
示例代码:
import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Person { private String name; private int age; // 构造方法、getter和setter等 // 省略其他代码 public static void main(String[] args) { List<Person> personList = new ArrayList<>(); personList.add(new Person("Alice", 25)); personList.add(new Person("Bob", 30)); personList.add(new Person("Charlie", 20)); Collections.sort(personList, Comparator.comparing(Person::getAge)); for (Person person : personList) { System.out.println(person.getName() + ", " + person.getAge()); } } }在上述示例中,我们使用
Comparator.comparing()方法来创建一个比较器,以根据年龄属性进行比较。然后,我们调用Collections.sort()方法并传入比较器,以对personList进行排序。 -
-
集合中常用的查找和替换操作有哪些?
答:在集合中进行查找和替换操作常见的方法有:
-
查找操作:
- 使用
contains(Object obj)方法:判断集合中是否包含指定的元素,返回布尔值。 - 使用
indexOf(Object obj)方法:获取指定元素在集合中的索引,若不存在则返回 -1。 - 使用
containsKey(Object key)方法(Map集合特有):判断Map集合中是否包含指定的键。 - 使用
containsValue(Object value)方法(Map集合特有):判断Map集合中是否包含指定的值。
- 使用
-
替换操作:
- 使用
set(int index, E element)方法(List集合特有):替换指定索引位置的元素。 - 使用
replace(Object oldValue, Object newValue)方法(Map集合特有):替换指定键的值。 - 使用
put(K key, V value)方法(Map集合特有):插入键值对,如果键已存在,则会替换对应的值。
- 使用
需要注意的是,集合中的查找操作通常会基于元素的相等性进行判断,因此在使用时需要确保元素正确实现了 equals() 和 hashCode() 方法。对于替换操作,要注意替换的范围和规则,以免引起意外的数据变动。
- 集合中常用的过滤和筛选操作有哪些?
答:在集合中进行过滤和筛选操作常见的方法有:
- 使用
filter(Predicate<? super T> predicate)方法(Java 8+):根据指定的条件筛选集合中的元素,并返回符合条件的新集合。
示例代码:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
- 使用
removeIf(Predicate<? super E> filter)方法:根据指定的条件过滤集合中的元素,并移除不符合条件的元素。
示例代码:
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
numbers.removeIf(n -> n % 2 != 0);
- 使用
stream()方法和流操作(Java 8+):使用流操作的各种方法,如map()、distinct()、limit()、skip()等,对集合进行过滤和筛选操作。
示例代码:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eva");
List<String> filteredNames = names.stream()
.filter(name -> name.length() <= 4)
.map(String::toUpperCase)
.collect(Collectors.toList());
这些方法提供了灵活的方式来根据条件对集合进行过滤和筛选,使得代码更简洁和可读。需要注意的是,过滤和筛选操作不会改变原始集合,而是返回一个新的集合或移除不符合条件的元素。
- 集合中常用的转换和拷贝操作有哪些?
答:在集合中进行转换和拷贝操作常见的方法有:
-
使用构造方法或静态方法进行集合之间的转换:
- 使用
ArrayList(Collection<? extends E> c)构造方法:将指定集合的所有元素添加到新的ArrayList集合中。 - 使用
LinkedList(Collection<? extends E> c)构造方法:将指定集合的所有元素添加到新的LinkedList集合中。 - 使用
HashSet(Collection<? extends E> c)构造方法:将指定集合的所有元素添加到新的HashSet集合中。 - 使用
TreeSet(Collection<? extends E> c)构造方法:将指定集合的所有元素添加到新的TreeSet集合中。 - 使用
HashMap(Map<? extends K, ? extends V> m)构造方法:将指定映射的所有键值对添加到新的HashMap集合中。 - 使用
TreeMap(SortedMap<K, ? extends V> m)构造方法:将指定映射的所有键值对添加到新的TreeMap集合中。
- 使用
-
使用
addAll(Collection<? extends E> c)方法:将指定集合的所有元素添加到目标集合中。
示例代码:
List<Integer> sourceList = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> targetList = new ArrayList<>();
targetList.addAll(sourceList);
- 使用流操作(Java 8+)进行集合的转换和拷贝:
- 使用
stream()方法将集合转换为流,然后使用流操作进行转换和拷贝。
示例代码:
- 使用
List<Integer> sourceList = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> targetList = sourceList.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
这些方法可以方便地实现集合之间的转换和拷贝操作,使得代码更简洁和高效。需要注意的是,转换和拷贝操作可能会创建新的集合对象或改变原始集合对象,具体取决于所使用的方法和操作。
- 集合中常用的合并和分割操作有哪些?
答:在集合中进行合并和分割操作常见的方法有:
-
合并操作:
- 使用
addAll(Collection<? extends E> c)方法:将一个集合中的所有元素添加到另一个集合中,实现集合的合并。
示例代码:
List<Integer> list1 = Arrays.asList(1, 2, 3); List<Integer> list2 = Arrays.asList(4, 5, 6); List<Integer> mergedList = new ArrayList<>(); mergedList.addAll(list1); mergedList.addAll(list2); - 使用
-
分割操作:
- 使用
subList(int fromIndex, int toIndex)方法:从一个集合中截取指定范围的子集合。
示例代码:
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); List<Integer> sublist = list.subList(2, 5); - 使用
需要注意的是,合并操作可以通过添加元素到目标集合来实现,而分割操作是通过截取源集合中的一部分元素生成一个新的子集合。在进行分割操作时,子集合与源集合共享内存,对子集合的修改会影响到源集合,因此需要注意操作的安全性和一致性。
第五章:文件和IO流
文件和IO流是Java中用于处理输入和输出的重要概念。文件代表着存储在外部设备上的数据,而IO流是用于在程序中读取和写入文件数据的通道。
文件:
- 文件是存储在外部设备上的数据集合,可以是文本文件、图像文件、音频文件等。
- Java中使用
File类表示文件对象,它提供了许多方法用于操作文件的属性和内容。
IO流:
- IO流是用于在程序中进行输入和输出操作的通道,将数据从文件或其他数据源读取到程序中,或将程序中的数据写入到文件或其他数据目的地。
- IO流分为输入流和输出流两种类型,分别用于读取数据和写入数据。
- Java提供了丰富的IO流类和接口,用于不同类型的输入和输出操作,如文件IO、网络IO等。
常见的IO流类包括:
- 字节流:
InputStream、OutputStream、FileInputStream、FileOutputStream等。 - 字符流:
Reader、Writer、FileReader、FileWriter等。 - 缓冲流:
BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream等。
IO流的操作包括:
- 读取操作:从输入流中读取数据到程序中,常见的方法有
read()、readLine()、read(byte[])等。 - 写入操作:将程序中的数据写入输出流,常见的方法有
write()、println()、write(byte[])等。 - 关闭操作:使用完IO流后,应该及时关闭流资源,释放系统资源,常见的方法有
close()。
1. 文件和目录操作
文件和目录操作是对计算机文件系统中的文件和目录进行创建、删除、重命名、移动等操作的过程。在Java中,我们可以使用java.io.File类提供的方法来进行文件和目录操作。
以下是文件和目录操作的一些方法:
-
boolean createNewFile(): 创建一个新的文件。 -
boolean mkdir(): 创建一个目录。 -
boolean mkdirs(): 创建多层目录。 -
boolean delete(): 删除文件或目录。 -
boolean renameTo(File dest): 用于重命名或移动文件或文件夹。 -
boolean exists(): 判断文件或目录是否存在。 -
boolean isDirectory(): 判断是否是一个目录。 -
boolean isFile(): 判断是否是一个文件。 -
boolean canRead(): 判断文件是否可读。 -
boolean canWrite(): 判断文件是否可写。 -
public boolean isHidden(): 判断文件是否为隐藏。 -
public String getAbsolutePath():返回文件或目录的绝对路径。绝对路径是从文件系统的根目录开始的完整路径。
-
public String getPath()返回文件或目录的路径。如果使用相对路径创建File对象,则返回相对路径;如果使用绝对路径创建File对象,则返回绝对路径。
-
public String getName()返回文件或目录的名称。对于文件,返回文件名和扩展名;对于目录,返回目录名称。
-
public long length():返回文件的长度(字节数),如果文件不存在或是目录,则返回0。 -
public long lastModified():返回文件或目录的最后修改时间,返回一个long型的时间戳。 -
String[] list(): 返回目录中的文件和目录的名称数组。 -
File[] listFiles(): 返回目录中的文件和目录的File对象数组。
示例代码:
import java.io.File;
public class FileExample {
public static void main(String[] args) {
File file = new File("path/to/file.txt");
// 判断文件是否存在
if (file.exists()) {
System.out.println("文件已存在");
} else {
System.out.println("文件不存在");
// 创建新文件
try {
if (file.createNewFile()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件创建失败");
}
} catch (Exception e) {
System.out.println("文件创建出错: " + e.getMessage());
}
}
}
}
下面是一个递归遍历当前目录并打印所有子目录和文件名的示例:
import java.io.File;
public class DirectoryTraversal {
public static void main(String[] args) {
File currentDirectory = new File(".");
traverseDirectory(currentDirectory);
}
public static void traverseDirectory(File directory) {
// 获取当前目录中的文件和子目录列表
File[] files = directory.listFiles();
// 遍历文件和目录
for (File file : files) {
// 打印文件或目录的名称
System.out.println(file.getName());
// 递归遍历子目录
if (file.isDirectory()) {
traverseDirectory(file);
}
}
}
}
在这个示例中,traverseDirectory() 方法接受一个代表目录的 File 对象,并进行递归遍历。它首先打印当前文件或目录的名称,然后检查它是否为目录。如果是目录,则再次调用自身以遍历子目录。
请注意,示例中使用 . 表示当前目录,但你可以将其替换为你想要遍历的任何目录的路径。
2. 字节流
字节流是以字节为单位进行数据读写的流。在 Java 中,字节流主要由 InputStream 和 OutputStream 这两个抽象类以及它们的具体实现类组成。字节流适用于处理二进制数据,例如读取和写入图片、视频、音频等文件。
2.1InputStream
字节输入流的抽象类,用于读取字节数据。
- 常用方法:
int read() throws IOException:从输入流中读取一个字节的数据,并返回读取的字节数据(以 0 到 255 的整数表示),如果已经到达输入流的末尾,则返回 -1。int read(byte[] b) throws IOException:从输入流中最多读取 b.length 个字节的数据,并存储到字节数组 b 中,返回实际读取的字节数,如果已经到达输入流的末尾,则返回 -1。int read(byte[] b, int off, int len) throws IOException:从输入流中最多读取 len 个字节的数据,并存储到字节数组 b 的指定偏移量 off 处开始的位置上,返回实际读取的字节数,如果已经到达输入流的末尾,则返回 -1。long skip(long n) throws IOException:跳过输入流中的 n 个字节的数据,返回实际跳过的字节数。int available() throws IOException:返回输入流中还可以读取(跳过)的字节数。void close() throws IOException:关闭输入流。void mark(int readlimit):在当前位置设置流中的标记,readlimit参数表示在标记失效之前可以读取的字节数。void reset() throws IOException:将输入流的位置重置到最后一次设置的标记位置。boolean markSupported():判断输入流是否支持标记和重置操作。
- 常用的子类
FileInputStream、ByteArrayInputStream:FileInputStream构造方法FileInputStream(File file):使用指定的 File 对象创建一个FileInputStream。它打开一个连接到实际文件的输入流。FileInputStream(String name):使用指定的文件名创建一个FileInputStream。它打开一个连接到实际文件的输入流。
ByteArrayInputStream构造方法ByteArrayInputStream(byte[] buf):使用指定的字节数组创建一个ByteArrayInputStream。它将该字节数组作为数据源。ByteArrayInputStream(byte[] buf, int offset, int length):使用指定的字节数组创建一个ByteArrayInputStream,并指定其数据源的起始偏移量和长度。它从指定偏移量开始读取指定长度的字节。
示例代码:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class InputStreamDemo {
public static void main(String[] args) {
try {
InputStream inputStream = new FileInputStream("input.txt");
int data;
while ((data = inputStream.read()) != -1) {
System.out.print((char) data);
}
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.2OutputStream
字节输出流的抽象类,用于写入字节数据。
- 包含方法:
void write(int b):将指定的字节写入输出流。写入的字节表示为 int 类型的参数 b 的低 8 位。void write(byte[] b):将指定字节数组中的数据写入输出流。void write(byte[] b, int off, int len):将指定字节数组中从偏移量 off 开始的 len 个字节写入输出流。void flush():刷新输出流。将缓冲区中的数据写入输出源,但输出流仍然可用。void close():关闭输出流。刷新缓冲区并释放与输出流相关的任何系统资源。
- 常用的子类包括
FileOutputStream、ByteArrayOutputStream:- 构造方法:
FileOutputStream(File file):创建一个新的文件输出流对象,以向指定文件写入内容。如果文件不存在,则创建该文件。FileOutputStream(String name):创建一个新的文件输出流对象,以向指定文件路径写入内容。如果文件不存在,则创建该文件。
- 构造方法:
示例代码:
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class OutputStreamMethodsDemo {
public static void main(String[] args) {
try {
OutputStream os = new FileOutputStream("output.txt");
// 写入单个字节
os.write(65);
// 写入字节数组
byte[] data = {66, 67, 68, 69};
os.write(data);
// 写入字节数组的一部分
byte[] partialData = {70, 71, 72, 73};
os.write(partialData, 1, 2);
// 刷新输出流
os.flush();
// 关闭输出流
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. 字符流
字符流是以字符为单位进行数据读写的流。在 Java 中,字符流主要由 Reader 和 Writer 这两个抽象类以及它们的具体实现类组成。字符流适用于处理文本数据,例如读取和写入文本文件。
| 基类 | Reader | Writer |
|---|---|---|
| 继承上一层 | InputStreamReader | OutputStreamWriter |
| 继承上一层 | FileReader | FileWriter |
在这个继承关系中:
Reader是字符输入流的抽象基类,提供了读取字符数据的方法。InputStreamReader是将字节流转换为字符流的桥梁,它继承自Reader。FileReader是用于读取字符文件的类,它继承自InputStreamReader。Writer是字符输出流的抽象基类,提供了写入字符数据的方法。OutputStreamWriter是将字符流转换为字节流的桥梁,它继承自Writer。FileWriter是用于写入字符文件的类,它继承自OutputStreamWriter。
3.1 转换流
转换流用于实现字符流和字节流之间的转换。
常用的转换流包括 InputStreamReader 和 OutputStreamWriter。
-
InputStreamReader将字节流转换为字符流,构造方法如下:InputStreamReader(InputStream in):使用系统默认字符集创建一个新的InputStreamReader对象,并将其连接到提供的InputStream。InputStreamReader(InputStream in, Charset cs):使用指定的字符集创建一个新的InputStreamReader对象,并将其连接到提供的InputStream。InputStreamReader(InputStream in, CharsetDecoder dec):使用指定的字符集解码器创建一个新的InputStreamReader对象,并将其连接到提供的InputStream。InputStreamReader(InputStream in, String charsetName):使用指定的字符集名称创建一个新的InputStreamReader对象,并将其连接到提供的InputStream。
-
OutputStreamWriter将字符流转换为字节流,构造方法如下:OutputStreamWriter(OutputStream out):使用系统默认字符集创建一个新的OutputStreamWriter对象,并将其连接到提供的OutputStream。OutputStreamWriter(OutputStream out, Charset cs):使用指定的字符集创建一个新的OutputStreamWriter对象,并将其连接到提供的OutputStream。OutputStreamWriter(OutputStream out, CharsetEncoder enc):使用指定的字符集编码器创建一个新的OutputStreamWriter对象,并将其连接到提供的OutputStream。OutputStreamWriter(OutputStream out, String charsetName):使用指定的字符集名称创建一个新的OutputStreamWriter对象,并将其连接到提供的OutputStream。
-
示例 : 使用
InputStreamReader将字节流转换为字符流
// 创建 FileInputStream 对象,用于读取字节流
FileInputStream fis = new FileInputStream("input.txt");
// 创建 InputStreamReader 对象,将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(fis);
// 创建 BufferedReader 对象,用于读取字符流
BufferedReader br = new BufferedReader(isr);
// 读取文件内容
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
// 关闭流
br.close();
- 示例 : 使用
OutputStreamWriter将字符流转换为字节流
// 创建 FileOutputStream 对象,用于写入字节流
FileOutputStream fos = new FileOutputStream("output.txt");
// 创建 OutputStreamWriter 对象,将字符流转换为字节流
OutputStreamWriter osw = new OutputStreamWriter(fos);
// 创建 BufferedWriter 对象,用于写入字符流
BufferedWriter bw = new BufferedWriter(osw);
// 写入文件内容
bw.write("Hello, World!");
bw.newLine();
bw.write("This is an example.");
// 刷新缓冲区并关闭流
bw.flush();
bw.close();
在示例中,我们首先创建了一个字节流对象(FileInputStream 或 FileOutputStream),然后使用相应的流对象创建 InputStreamReader 或 OutputStreamWriter 对象,进行字节流和字符流之间的转换。最后,我们可以使用字符流对象进行读取或写入操作。
3.2 Reader
字符输入流的抽象类,用于读取字符数据。
- 包含方法:
int read() throws IOException:从输入流中读取一个字符,并返回其对应的 Unicode 值。如果已达到流的末尾,则返回 -1。int read(char[] cbuf) throws IOException:从输入流中读取字符到字符数组cbuf中,并返回实际读取的字符数。如果已达到流的末尾,则返回 -1。int read(char[] cbuf, int off, int len) throws IOException:从输入流中读取字符到字符数组cbuf的指定位置,并返回实际读取的字符数。如果已达到流的末尾,则返回 -1。long skip(long n) throws IOException:跳过输入流中的 n 个字符,并返回实际跳过的字符数。boolean ready() throws IOException:检查输入流是否准备好被读取。boolean markSupported():判断输入流是否支持标记操作。void mark(int readAheadLimit) throws IOException:在当前位置设置一个标记,并指定标记操作的限制。void reset() throws IOException:将输入流的位置重置到最近的标记位置。void close() throws IOException:关闭输入流。
- 常用子类
FileReader构造方法:FileReader(File file):使用指定的File对象创建FileReader对象。FileReader(String fileName):使用指定的文件名创建FileReader对象。
3.3 Writer
字符输出流的抽象类,用于写入字符数据。
-
包含方法
void write(int c) throws IOException:将指定的字符写入输出流。void write(char[] cbuf) throws IOException:将字符数组cbuf中的所有字符写入输出流。void write(char[] cbuf, int off, int len) throws IOException:将字符数组cbuf的指定范围内的字符写入输出流。void write(String str) throws IOException:将字符串str中的所有字符写入输出流。void write(String str, int off, int len) throws IOException:将字符串 str 的指定范围内的字符写入输出流。void flush() throws IOException:刷新输出流,将缓冲区中的数据立即写入目标设备。void close() throws IOException:关闭输出流。
-
常用子类
FileWriter构造方法:-
FileWriter(File file):使用指定的 File 对象创建FileWriter对象。 -
FileWriter(String fileName):使用指定的文件名创建FileWriter对象。 -
FileWriter(File file, boolean append):使用指定的 File 对象和追加标志创建FileWriter对象。 -
FileWriter(String fileName, boolean append):使用指定的文件名和追加标志创建FileWriter对象。
-
-
示例:使用
FileReader读取文件的内容并将其存储在字符数组中,然后使用FileWriter将字符数组中的内容写入另一个文件:import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; public class FileCopyExample { public static void main(String[] args) { String sourceFileName = "source.txt"; String destinationFileName = "destination.txt"; try (FileReader fileReader = new FileReader(sourceFileName); FileWriter fileWriter = new FileWriter(destinationFileName)) { char[] buffer = new char[1024]; int charsRead; // 从源文件读取字符数组 while ((charsRead = fileReader.read(buffer)) != -1) { // 将字符数组写入目标文件 fileWriter.write(buffer, 0, charsRead); } System.out.println("文件复制成功。"); } catch (IOException e) { e.printStackTrace(); } } }请注意,读取和写入的数据量取决于字符数组的大小。较大的字符数组可以提高性能,但会占用更多的内存。您可以根据实际需求调整字符数组的大小。
4. 缓冲流
缓冲流是在字节流和字符流的基础上提供了缓冲功能,可以减少对底层资源的访问次数,提高读写的效率。在 Java 中,缓冲流包括字节缓冲流和字符缓冲流。
5.4.1 BufferedInputStream 和 BufferedOutputStream
-
BufferedInputStream是字节缓冲输入流,继承自FilterInputStream用于提供带缓冲的字节读取功能。- 可以通过构造方法传入一个字节输入流来创建
BufferedInputStream。 - 常用方法有
read()、available()、close()等。
- 可以通过构造方法传入一个字节输入流来创建
-
BufferedOutputStream是字节缓冲输出流,继承自FilterOutputStream。用于提供带缓冲的字节写入功能。- 可以通过构造方法传入一个字节输出流来创建
BufferedOutputStream。 - 常用方法有
write()、flush()、close()等。
- 可以通过构造方法传入一个字节输出流来创建
5.4.2 BufferedReader 和 BufferedWriter
-
BufferedReader是字符缓冲输入流,继承自Reader,用于提供带缓冲的字符读取功能。- 可以通过构造方法传入一个字符输入流来创建
BufferedReader。 - 常用方法有
readLine()、ready()、close()等。
- 可以通过构造方法传入一个字符输入流来创建
-
BufferedWriter是字符缓冲输出流,继承自Writer,用于提供带缓冲的字符写入功能。- 可以通过构造方法传入一个字符输出流来创建
BufferedWriter。 - 常用方法有
write()、flush()、close()等。
- 可以通过构造方法传入一个字符输出流来创建
示例代码:
// 使用 BufferedInputStream 读取文件内容
try (InputStream is = new FileInputStream("input.txt");
BufferedInputStream bis = new BufferedInputStream(is)) {
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
// 使用 BufferedOutputStream 写入文件内容
try (OutputStream os = new FileOutputStream("output.txt");
BufferedOutputStream bos = new BufferedOutputStream(os)) {
String content = "Hello, World!";
byte[] bytes = content.getBytes();
bos.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
// 使用 BufferedReader 读取文件内容
try (Reader reader = new FileReader("input.txt");
BufferedReader br = new BufferedReader(reader)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
// 使用 BufferedWriter 写入文件内容
try (Writer writer = new FileWriter("output.txt");
BufferedWriter bw = new BufferedWriter(writer)) {
String content = "Hello, World!";
bw.write(content);
} catch (IOException e) {
e.printStackTrace();
}
以上示例中,我们使用了字节缓冲流(BufferedInputStream 和 BufferedOutputStream)和字符缓冲流(BufferedReader 和 BufferedWriter)来提高读写的效率。
5. 序列化
序列化是将对象的状态转换为可存储或传输的形式的过程。在Java中,可以使用序列化将对象转换为字节流,以便在需要时进行存储、传输或持久化。
序列化的过程将对象转换为字节流,而反序列化的过程则将字节流转换为对象。序列化和反序列化的过程需要使用输入流和输出流进行操作。
5.1 实现序列化的方式
在Java中,可以通过实现Serializable接口或自定义序列化方式来实现对象的序列化。
- 实现
Serializable接口
要使一个类可以被序列化,只需实现Serializable接口即可。Serializable接口是一个标记接口,没有定义任何方法,它的存在仅仅是为了告诉Java编译器该类是可序列化的。
-
自定义序列化方式
除了实现
Serializable接口,还可以通过自定义序列化方式来控制对象的序列化过程。这可以通过实现以下两个方法来实现:private void writeObject(ObjectOutputStream out) throws IOException:自定义对象的序列化过程,将对象的状态写入输出流。private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException:自定义对象的反序列化过程,从输入流中读取字节并将其转换为对象的状态。
5.2 对象引用的序列化
在进行对象的序列化和反序列化时,需要注意以下事项:
- 版本号的处理
在序列化过程中,对象的版本号是很重要的,它用于标识对象的版本信息。当对象的结构发生变化时,版本号的处理变得非常重要,以确保序列化和反序列化的兼容性。
public class MyClass implements Serializable {
private static final long serialVersionUID = 1L; // 版本号
// 其他成员和方法
}
-
transient关键字默认情况下,当序列化一个对象时,其引用的对象也会被序列化。然而,在某些情况下,我们可能希望对某些对象引用进行特殊处理,例如避免循环引用或只序列化对象的部分引用。为了处理对象引用的序列化,可以使用
transient关键字或自定义序列化方式。- 使用
transient关键字:可以将对象引用标记为瞬态,即不会被序列化。通过在对象引用前添加transient关键字,可以防止其被序列化。
public class MyClass implements Serializable { private transient OtherClass obj; // transient关键字标记为瞬态 // 其他成员和方法 }- 自定义序列化方式:可以通过自定义序列化和反序列化方法来控制对象引用的处理。在
writeObject和readObject方法中,可以手动控制对象引用的序列化和反序列化过程,从而实现特定的处理逻辑。
- 使用
public class MyClass implements Serializable {
private OtherClass obj;
private void writeObject(ObjectOutputStream out) throws IOException {
// 手动序列化对象引用
out.writeObject(obj.getId());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 手动反序列化对象引用
int id = (int) in.readObject();
obj = new OtherClass(id);
}
// 其他成员和方法
}
5.3 序列化的示例代码
下面是一个简单的示例代码,展示了对象的序列化和反序列化过程:
import java.io.*;
public class SerializationExample {
public static void main(String[] args) {
// 创建一个对象
Student student = new Student("John Doe", 25, "Computer Science");
// 序列化对象
serializeObject(student, "student.ser");
// 反序列化对象
Student deserializedStudent = (Student) deserializeObject("student.ser");
// 打印反序列化后的对象
System.out.println("Deserialized Student: " + deserializedStudent);
}
// 序列化对象
private static void serializeObject(Serializable object, String fileName) {
try (FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(object);
System.out.println("Object serialized successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
// 反序列化对象
private static Object deserializeObject(String fileName) {
try (FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream ois = new ObjectInputStream(fis)) {
Object object = ois.readObject();
System.out.println("Object deserialized successfully.");
return object;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
6. Properties集合
Properties集合是Java中用于处理属性的类,它继承自Hashtable类,并扩展了一些额外的方法。Properties集合主要用于读取和保存属性配置文件,例如配置文件、资源文件等。它以键值对的形式存储属性信息,并提供了方便的方法来添加、获取和操作属性。
6.1 Properties集合的常用方法
Properties集合提供了一些常用的方法来操作属性,包括添加和获取属性、遍历属性、加载和保存属性以及设置默认属性。
-
添加和获取属性
-
setProperty(String key, String value): 向Properties集合中添加一个属性,使用指定的键和值。 -
getProperty(String key): 根据指定的键获取对应的属性值。 -
getProperty(String key, String defaultValue): 根据指定的键获取对应的属性值,如果属性不存在,则返回默认值。
-
-
遍历属性
-
propertyNames(): 获取Properties集合中所有属性的键的枚举。 -
stringPropertyNames(): 获取Properties集合中所有属性的键的集合。 -
entrySet(): 获取Properties集合中所有属性的键值对的集合。
-
-
加载和保存属性
-
load(InputStream in): 从输入流中加载属性,将属性的键值对信息读取到Properties集合中。 -
store(OutputStream out, String comments): 将Properties集合中的属性保存到输出流中,以键值对的形式写入。
-
-
设置默认属性
setDefault(Properties defaults): 设置默认的属性集合,如果在获取属性时找不到对应的键,则会在默认属性集合中查找。
6.2 Properties集合与io流结合使用示例
下面是一个示例代码,演示了如何将Properties集合与IO流结合使用来读取和保存属性配置文件:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;
public class PropertiesExample {
public static void main(String[] args) {
// 读取属性配置文件
Properties properties = new Properties();
try (FileInputStream fis = new FileInputStream("config.properties")) {
properties.load(fis);
} catch (IOException e) {
e.printStackTrace();
}
// 获取属性值
String username = properties.getProperty("username");
String password = properties.getProperty("password");
System.out.println("Username: " + username);
System.out.println("Password: " + password);
// 修改属性值
properties.setProperty("password", "newpassword");
// 保存属性配置文件
try (FileOutputStream fos = new FileOutputStream("config.properties")) {
properties.store(fos, "Updated Properties");
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个示例中,我们首先创建了一个Properties对象。然后,通过load()方法从属性文件config.properties中加载属性。接下来,我们使用getProperty()方法获取属性值,并打印出来。
然后,我们修改了属性文件中的密码,使用setProperty()方法设置新的属性值。最后,通过store()方法将修改后的属性保存回文件。
请确保在运行示例代码之前,已经创建了名为config.properties的属性文件,并在其中定义了username和password属性。
多线程
- 线程的概念
- 创建线程的方式
- 线程的生命周期
- 线程同步和互斥
- 线程池
数据库连接和操作
- JDBC概述
- 连接数据库
- 执行SQL语句
- 事务处理

1295

被折叠的 条评论
为什么被折叠?



