重温java基础知识

前言

自从准备考试以后很久都没有整过我自己的老本行了,很多东西都忘得差不多了,我决定在这个学期把丢下的这些东西全都捡起来
还有硬件的一些专业知识,包括最新的ai都学习起来

快速整理:ctrl+alt+L
快速运行:ctrl+shift+F10
多行注释:ctrl+alt+/
单行:ctrl+/
构造方法等:alt+insert
try-catch: ctrl+alt+t
Math.sqrt() 用于计算平方根
ctrl + Alt+U 查看类图 ctrl + Alt+B

  • 斐波那契数列公式:
    F(0) = 0

F(1) = 1

F(n) = F(n-1) + F(n-2) (当 n ≥ 2)

  • 海伦公式计算三角形面积:
    计算三边的长度:a, b, c
    计算半周长:s = (a + b + c) / 2
    计算面积:area = sqrt(s * (s - a) * (s - b) * (s - c))

  • 求2的n次方可以用 int result = 1 << n; // 使用位运算:1左移n位

第一章数据类型转换

格式化输出

1
System.out.printf("a/b=%.2f\n", (a / b));//保留小数点后两位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package diyizhang;
//强制类型转换
public class App3_2 {
public static void main(String[] args) {
int a = 155 , b=6;
float g,h;
System.out.println("a="+a+",b="+b);
g=a/b;
System.out.println("g=a/b="+g);
System.out.println("a="+a+",b="+b);
h=(float) a/b;
System.out.println("h=a/b="+h);
System.out.println("(int) h="+(int)h);
/******************下面是文字转数据的**********************/
String name= "3.14";
float parseFloat = Float.parseFloat(name);
System.out.println(parseFloat);

}

}

精度保留

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 示例数据:计算 10/3 的结果
double result = 10.0 / 3;

// 1. 使用 System.out.printf() 直接输出
System.out.println("使用 printf() 保留小数:");
System.out.printf("保留2位小数:%.2f%n", result); // 输出 3.33
System.out.printf("保留5位小数:%.5f%n", result); // 输出 3.33333
System.out.printf("保留9位小数:%.9f%n", result); // 输出 3.333333333

// 2. 使用 String.format() 转换为字符串后再处理
System.out.println("\n使用 String.format() 保留小数:");
String str2 = String.format("%.2f", result);
String str5 = String.format("%.5f", result);
String str9 = String.format("%.9f", result);
System.out.println("保留2位小数:" + str2); // 输出 3.33
System.out.println("保留5位小数:" + str5); // 输出 3.33333
System.out.println("保留9位小数:" + str9); // 输出 3.333333333

// 处理用户输入的示例
Scanner scanner = new Scanner(System.in);
System.out.println("\n请输入两个整数(a b):");
int a = scanner.nextInt();
int b = scanner.nextInt();
double userResult = (double) a / b; // 注意先转换为double再计算
System.out.printf("结果保留6位小数:%.6f%n", userResult);

scanner.close();

局部变量的类型转换

重要部分:只有初始化变量时var才能用来声明变量,初始化变量就是赋一个值

1
2
var b;//这种写法是错的

数组,字符串,正则表达式

一维数组的定义

定义需要经过三个步骤:声明数组,数组分配内存空间,创建数组空间并且赋值

声明

数据类型[] 数组名
这里与c不同的是c是将 数组名和[] 调换了方向的。本人一开始学习的c,所以更倾向于c的写法

数组分配内存空间

分配内存给数组

1
2
数组名 = new 数据类型[个数]

数组创建后就不能修改其大小

一维数组的初始化

在定义数组的同时就为数组元素分配空间并赋值。

1
数据类型 [] 数组名 = {初值1,初值2,初值3,.....}

二维数组

二维数组初始化

1
2
3
4
5
6
7
8
9
10
11
// 方式1 分开来
int [][] a;
a = new [1][2];
// 第二种方式
int [][] a = new [1][2];

//正确的示范
int [][] a = new [1][];
int [][] a = new [1][2];
//错误示范
int [][] a = new [][2];

上述的错误示范其实很好理解
这是我问ai的出来的最通俗的理解
就是如果这个是一块地皮需要建楼
行代表我要建几层楼,至于每层楼建几米没有确定这个是能理解的
但是如果先说每层楼建几米,但是没说整栋楼要建基层是不对的这样就延申下来了以下的问题

杨辉三角形的问题里面

1
2
3
4
5
6
7
8
9
int i, j; //定义行列
int level = 7;
int[][] yh = new int[level][];
System.out.println("杨辉三角形");
for (i = 0; i< yh.length ; i++) {
yh[i] = new int[i+1];
}
yh[i] = new int[i+1]; //这段代码我就有点不理解

解释:
用搭积木的思维来理解
想象一下你要用积木搭建一个金字塔:

1
2
3
4
5
6
7
8
9
10
java
// 你说:我要建一个7层的金字塔
int level = 7;
int[][] pyramid = new int[level][]; // 先规划出7层的空间,但每层还是空的

// 现在开始为每一层放置合适长度的积木
for (int i = 0; i < pyramid.length; i++) {
pyramid[i] = new int[i + 1]; // 第0层放1块积木,第1层放2块,以此类推
}

这个过程就像:

你先申请了一块地,说这里要盖7层楼 (new int[7][])

然后你从第一层开始,每层搭建不同长度的墙体

第一层:搭建1米长的墙 (new int[1])

第二层:搭建2米长的墙 (new int[2])

第七层:搭建7米长的墙 (new int[7])

字符串

String型字符串

字符常量 使用单引号 ‘’ 括起来,并且里面必须且只能有一个字符。

1
2
3
4
5
6
7
char letter = 'A';      // 正确:一个大写字母A
char digit = '9'; // 正确:一个数字字符
char symbol = '#'; // 正确:一个符号
char ch = ' '; // 正确:一个空格字符
// char error = 'AB'; // 错误:单引号内只能有一个字符
// char error = ''; // 错误:单引号内不能为空
字符串常量 使用双引号 "" 括起来,里面的内容可以非常灵活。
1
2
3
4
String greeting = "Hello, World!"; // 正确:多个字符组成的序列
String single = "A"; // 正确:哪怕只有一个字符,也是字符串
String empty = ""; // 正确:空字符串,长度为0
String number = "123"; // 正确:这是字符串"123",不是数字123

类与对象

参数的传递

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


class Cylinder {
double radius, pi;
int height;

void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

public class App6_4 {
public static void main(String[] args) {
Cylinder volu = new Cylinder();
volu.setCylinder(2.5, 5, 3.14);
System.out.println("底半径:" + volu.radius);
System.out.println("圆柱体的高:" + volu.height);
System.out.println("圆周率:" + volu.pi);
System.out.println("圆柱体");
System.out.println("底面积:" + volu.area());
System.out.println("圆柱体体积:" + volu.volume());
}

}

在这里有个问题是如果我少传一个参数该怎么办

1
2
volu.setCylinder(2.5, 5, 3.14);

很显然,这个是不能的。但是我们可以使用方法重载

1
2
3
4
5
6
7
8
9
10
11
void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}
void setCylinder(double radius, int h, ) { //重载后的
this.radius=radius;
radius = r;
height = h;
}

this的使用

1
2
3
4
5
6
7
8
9
10
11
class Cylinder {
double radius, pi;
int height;

void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}

这里的radius成员变量和局部变量是相同的变量,如果我成员变量用作算其他的式子,局部变量算另一个式子。这样就会出现混乱
当特指成员变量时,要用this关键字

以数组作为参数进行传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class App6_5{
public static void main(String[] args){
int[] a={8,3,7,88,9,23};
LeastNumb minNumber=new LeastNumb();
minNumber.least(a);
}
}
class LeastNumb{
public void least(int[] array){
int temp=array[0];
for(int i=1;i<array.length;i++)
if(temp>array[i])
temp=array[i];
System.out.println("最小的数为:"+temp);
}
}

从例子中看出,如果想要将参数传递到方法中,只需要在方法名后面传入数组名就可以了。
如果传入的时

1
2
minNumber.least(a[1]);
这个就不满足条件了,这里需要的是一个数组类型的,然后提供的是int 类型的,即不满足对应条件

返回值为数组类型的方法

若方法需要返回一个数组,则必须在该方法名之前加上数组类型的修饰符,例如返回一维数组
int [] ,返回二维数组 int [][]

1
2
3
4
5
6
7
8
9
 int [] test(int [] a){

return a;
}

int [][] test2(int [][] b){
return b;
}

可变参数

参数的接收可以是固定的,或者不固定的。方法接收不固定的情况则为可变参数
方法接收可变参数的方法为

1
2
3
返回值类型 方法名 (固定参数列表,数据类型 ...可变参数名){
方法体
}

当有多个参数的情况下,可变参数只能位于最后一个
调用可变参数的时候,编译器为可变参数创建一个隐含的数组,通过调用数组的形式来访问可变参数

匿名对象

当一个对象被创建时可以不用那么麻烦的创建一个对象的引用变量,直接调用对象的方法

1
2
3
//这个是原始的写法
Cylinder cy = new Cylinder()
cy.setCylinder(11111)
1
2
//这个是匿名对象的写法
new Cylinder().setCylinder(111111)

使用场景:

  • 这个对象只需要一次调用的话就可以这样使用
  • 将匿名对象作为实参传递给一个方法调用。

java语言类的特性

类的私有成员与公共成员

无访问控制符

若在类成员的前面不加任何访问控制符,则该成员具有默认访问控制符
表示这个成员变量只能在相同的包内进行访问和调用

方法重载

方法的含义相同,但带有不同的参数,这些方法使用相同的名字,这就叫方法的重载

  • 注意:
    仅仅参数的变量名不同是不行的
    参数个数不同,参数的类型不同,参数的顺序不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Cylinder{
private double radius;
private int height;
private double pi=3.14;
private String color;
public double setCylinder(double r,int h){
radius=r;
height=h;
return r+h;
}
public void setCylinder(String str){
color=str;
}
public void show(){
System.out.println(color);
}
double area(){
return pi*radius*radius;
}
double volume(){
return area()*height;
}
}

构造方法详解

我理解你对构造方法这个概念感到困惑。让我通过你的代码示例来详细解释构造方法的作用和特点。

构造方法的基本概念

构造方法是一种特殊的方法,它在创建对象时自动调用,主要用于初始化对象的属性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cylinder {
private double radius;
private int height;
private double pi = 3.14;

// 这是构造方法
public Cylinder(double r, int h) {
radius = r;
height = h;
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

构造方法重载

在类中同时存在无参构造,有参构造这种就算构造方法重载

从一个构造方法调用另一个构造方法

在类中有多个方法,这些方法中可以相互调用,通过使用this()语句来调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//FileName: App7_5.java
class Cylinder {
private double radius;
private int height;
private double pi = 3.14;
String color;

public Cylinder() {
this(2.5, 5, "蓝色");
System.out.println("无参构造方法被调用");
}

public Cylinder(double r, int h, String str) {
System.out.println("有参构造方法被调用");
radius = r;
height = h;
color = str;
}

public void show() {
System.out.println("圆柱体底半径为:" + radius);
System.out.println("圆柱体的高为:" + height);
System.out.println("圆柱体的颜色为:" + color);
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

public class App7_5 {
public static void main(String[] args) {
Cylinder volu = new Cylinder();
System.out.println("圆柱体底面积=" + volu.area());
System.out.println("圆柱体体积=" + volu.volume());
volu.show();
}
}

staticfinal 的组合作用

staticfinal 虽然经常一起使用,但它们的作用是完全不同的。

两者的区别

关键字 作用 解决的问题
static 属于类而不是对象 内存分配问题
final 不可被修改 数值不变性问题

具体分析

只有 final(没有 static)的情况

1
public final double PI = 3.1415926535;

这种情况下:

  • 每个 MathUtils 对象都有自己的 PI 副本
  • 创建100个对象就有100个 PI 变量在内存中
  • 浪费内存空间

只有 static(没有 final)的情况

1
public static double PI = 3.1415926535;

这种情况下:

  • 只有一个 PI 变量在内存中(节省内存)
  • 但是这个值可以被修改:MathUtils.PI = 4.0; (危险!)

static + final 的完美组合

1
public static final double PI = 3.1415926535;

这种情况下:

  • ✅ 只有一个变量在内存中(节省内存)
  • ✅ 数值不可被修改(保证数学常量的正确性)
  • ✅ 通过类名直接访问:MathUtils.PI

实际内存对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
// 实例常量 - 每个对象都有一份
public final int INSTANCE_CONST = 100;

// 静态常量 - 只有一份
public static final int STATIC_CONST = 200;
}

// 使用
Test obj1 = new Test();
Test obj2 = new Test();
Test obj3 = new Test();

// 内存中有:
// - 3个 INSTANCE_CONST(每个对象一个)
// - 1个 STATIC_CONST(整个类共享)

为什么数学常量需要这样设计?

1. 数学真实性

π的值是固定不变的,不应该允许被修改

1
2
3
// 如果只有static,可能发生:
MathUtils.PI = 4.0; // 数学灾难!
double area = MathUtils.PI * radius * radius; // 错误的结果

2. 内存效率

π的值对所有对象都是一样的,不需要每个对象都存储一份

1
2
3
4
5
// 如果没有static:
MathUtils util1 = new MathUtils();
MathUtils util2 = new MathUtils();
MathUtils util3 = new MathUtils();
// 内存中有3个相同的PI值,浪费空间

3. 访问便利性

不需要创建对象就能使用

1
2
3
4
5
6
// 有static:直接使用
double area = MathUtils.PI * radius * radius;

// 没有static:需要先创建对象
MathUtils utils = new MathUtils();
double area = utils.PI * radius * radius; // 麻烦且浪费

其他常见的使用场景

颜色常量

1
2
3
4
5
6
public class Colors {
public static final String RED = "#FF0000";
public static final String GREEN = "#00FF00";
public static final String BLUE = "#0000FF";
}
// 使用:Colors.RED

错误代码

1
2
3
4
5
6
public class ErrorCodes {
public static final int SUCCESS = 0;
public static final int FILE_NOT_FOUND = 404;
public static final int PERMISSION_DENIED = 403;
}
// 使用:ErrorCodes.SUCCESS

配置参数

1
2
3
4
5
6
public class Config {
public static final int MAX_CONNECTIONS = 100;
public static final int TIMEOUT = 30000;
public static final String DATABASE_URL = "jdbc:mysql://localhost/db";
}
// 使用:Config.MAX_CONNECTIONS

总结

static final 组合的意义:

  • **static**:确保只有一个副本,节省内存,方便访问
  • **final**:确保数值不会被意外修改,保证数据完整性

对于真正的常量(数学常数、配置参数、状态码等),总是应该使用 public static final 的组合。

通俗来说,static就是整个类在内存中占领一个空间,static在类中就是一个公共的谁都可以使用的,只需要占一个内存就可以了

Java 自动装箱与拆箱机制

类型与包装类类型之间的自动转换

讲真的这个知识点之前一直在用,但是如此专业的名字我还真的不知道,之前用的时候也不知道为什么要这样用
于是我问了ai得出了下面的结果

概述

在官方文档中称为装箱(Boxing)拆箱(Unboxing),合称为自动装箱(Autoboxing)

这个特性是 JDK 5 引入的,极大地方便了我们的编码。

1. 核心概念

操作 转换方向 描述 示例
装箱 (Boxing) 基本类型包装类 将基本数据类型的值自动封装(转换)成其对应的包装类对象。 Integer i = 10;
拆箱 (Unboxing) 包装类基本类型 将包装类对象自动解封(转换)成其对应的基本数据类型值。 int num = i;

在没有自动装箱之前,这些操作都需要手动完成。

JDK 5 之前(手动):

1
2
Integer i = Integer.valueOf(10); // 手动装箱
int num = i.intValue(); // 手动拆箱

JDK 5 之后(自动):

1
2
Integer i = 10;  // 自动装箱:编译器背后执行的仍是 Integer.valueOf(10)
int num = i; // 自动拆箱:编译器背后执行的仍是 i.intValue()

2. 对应的类型关系表

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

3. 发生的常见场景

a) 赋值时(最常见)

这是最直接的自动装箱和拆箱。

1
2
3
// 赋值时的装箱和拆箱
Double d = 3.14; // 自动装箱:double -> Double
double pi = d; // 自动拆箱:Double -> double

b) 方法调用时(非常重要)

当传入方法的参数类型与实际值类型不匹配时,会自动发生。

1
2
3
4
5
6
7
8
public static void printInteger(Integer i) {
System.out.println(i);
}

public static void main(String[] args) {
int num = 5;
printInteger(num); // 自动装箱:int -> Integer,然后传入方法
}
1
2
3
4
5
6
7
8
9
public static int add(int a, int b) {
return a + b;
}

public static void main(String[] args) {
Integer a = 10;
Integer b = 20;
int sum = add(a, b); // 自动拆箱:Integer -> int,然后传入方法
}

c) 集合操作时(使用频率极高)

Java 的集合框架(如 ArrayList, HashMap)只能存储对象,不能存储基本类型。自动装箱让我们可以像直接存基本类型一样方便。

1
2
3
4
5
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱:int -> Integer
list.add(2); // 自动装箱

int first = list.get(0); // 自动拆箱:Integer -> int

d) 运算和比较时

在数学运算中,包装类会自动拆箱为基本类型进行计算。在三元运算符、==>< 等混合运算中,也会发生自动拆箱和装箱。

1
2
3
4
5
Integer a = 10;
Integer b = 20;
int result = a + b; // 先自动拆箱 a->int, b->int,然后计算,最后将int结果赋值给result

Integer c = (a > b) ? a : b; // 比较时拆箱,赋值时装箱

4. 注意事项和陷阱(面试常考!)

自动装箱虽然方便,但也带来了一些容易忽略的问题。

陷阱一:== 比较的陷阱

== 在比较对象时,比较的是内存地址(引用是否指向同一个对象),而不是值。

情况1:值的范围在 [-128, 127] 之间

Java 对这部分常用的 Integer 对象进行了缓存,Integer.valueOf() 会返回缓存中的对象。

1
2
3
4
5
6
7
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true (因为指向缓存中的同一个对象)

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false (因为超出了缓存范围,是新new的对象)

情况2:任何范围,== 比较包装类和基本类型

这时包装类会自动拆箱,然后比较值。

1
2
3
Integer a = 128;
int b = 128;
System.out.println(a == b); // true (a被拆箱为int,然后进行值比较)

最佳实践:比较包装类的值,永远使用 .equals() 方法!

1
2
3
Integer a = 128;
Integer b = 128;
System.out.println(a.equals(b)); // true (比较的是值)

陷阱二:空指针异常(NullPointerException)

包装类可以是 null,而基本类型不能。如果在拆箱一个 null 的包装类,就会抛出异常。

1
2
Integer nullInteger = null;
int num = nullInteger; // 运行时抛出 NullPointerException

这种情况常发生在从方法返回 null 或从集合中取出 null 值时,需要格外小心。

陷阱三:性能开销

虽然微小,但装箱和拆箱确实会创建对象和调用方法,存在额外的性能开销。在大量的循环(如千万次、亿次)中,这种开销会累积变得显著。

  • 高性能场景: 应优先使用基本类型(如 int),避免无意识的自动装箱。
  • 一般业务场景: 为了方便,使用自动装箱完全没有问题。

Java 包装类与自动装箱的应用场景

概述

简单来说,使用包装类是为了弥补基本类型的局限性,让它们能在”面向对象”的世界里更好地工作。而自动装箱/拆箱是为了消除使用包装类时的繁琐代码,让我们写起来更方便。

核心应用场景

场景一:让基本类型能存入集合(最核心的原因)

这是最常用、最重要的原因。Java的集合框架(如 ArrayList, HashMap, HashSet)在设计上有一个基本原则:它们只能存储对象(Object),不能存储基本类型(int, double 等)。

为什么集合不能存基本类型?

因为集合的设计需要通用性,要能存放任何类型的数据。而”任何类型”在Java里最顶层的父类就是 Object。基本类型不是对象,不从 Object 继承,所以被排除在外。

没有包装类和自动装箱时(JDK5以前):

1
2
3
4
5
6
7
8
9
10
// 1. 想存一个int到ArrayList里,必须先手动"装箱"
ArrayList list = new ArrayList();
int age = 25;
Integer ageInteger = Integer.valueOf(age); // 手动打包成对象
list.add(ageInteger); // 才能存进去

// 2. 想取出来用,必须先手动"拆箱"
Integer result = (Integer) list.get(0); // 取出来是Object,要强转
int myAge = result.intValue(); // 再手动解包成基本类型才能计算
System.out.println(myAge + 1); // 输出26

太麻烦了! 每次存取的代码都变得很长。

有了包装类和自动装箱后:

1
2
3
4
5
// 现在我们可以这样写,直观又简洁
ArrayList<Integer> list = new ArrayList<>(); // 指定泛型为Integer
list.add(25); // 自动装箱:编译器帮你变成 list.add(Integer.valueOf(25));
int myAge = list.get(0); // 自动拆箱:编译器帮你变成 list.get(0).intValue();
System.out.println(myAge + 1); // 输出26

结论: 为了让基本数据类型也能方便地存入集合这种强大的容器中,我们必须使用包装类,而自动装箱/拆箱让这个过程毫不费力。


场景二:表示”缺失”或”未知”的值(空值)

基本类型都有默认值(如 int 默认是 0)。但在很多业务场景下,0 是一个有效的数值,而不是”未知”或”未设置”。

例子:统计学生年龄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用基本类型 int - 有歧义
int studentAge = 0; // 他到底是0岁,还是年龄信息还没录?

// 使用包装类 Integer - 含义清晰
Integer studentAge = null; // 明确表示:年龄未知,尚未录入

// 后续处理逻辑可以这样写
if (studentAge == null) {
System.out.println("请录入该学生年龄");
} else if (studentAge > 18) {
System.out.println("成年人");
} else {
System.out.println("未成年人");
}

结论: 包装类可以用 null 来表示值的缺失,这在数据库查询、JSON解析等场景中极其常见(因为数据库中的字段很多就是可空的)。


场景三:使用泛型时

泛型(Generics)是JDK5另一个重大特性,它也和集合一样,类型参数必须是类类型,不能是基本类型

你不能这样写:

1
List<int> list = new ArrayList<>(); // 编译错误!

你只能这样写:

1
List<Integer> list = new ArrayList<>(); // 正确!

所以,只要你使用泛型(无论是集合还是自己定义的泛型类/方法),涉及到基本类型时,就必须使用其包装类。


场景四:使用方法时,需要对象而非值

有些API或方法设计就是要求传入一个对象,以便进行一些内部操作(比如同步锁、或需要修改传入的参数值)。

例如:使用同步锁

1
2
3
4
5
6
7
8
// 你可以用一个Integer对象作为锁(虽然不常见,但可行)
private final Integer lockId = 100;

public void someMethod() {
synchronized(lockId) { // synchronized需要传入一个对象
// 线程安全的代码
}
}

虽然基本类型 int 不能这么用,但它的包装类 Integer 可以。


总结:如何选择用哪个?

特性 基本类型 (e.g., int) 包装类 (e.g., Integer)
本质 纯数据值 对象
存储 在栈内存,效率极高 在堆内存,有开销
默认值 有(如 int0 null
用途 性能优先的场景 功能优先的场景

日常编码指南:

  • 定义类的成员属性:优先考虑包装类
  • 定义方法的局部变量:优先使用基本类型
  • 使用集合和泛型:必须使用包装类

JAVA语言垃圾回收机制

在整个java运行机制中,java运行环境提供了一套java回收机制。
其实核心就是计算调用的次数,每个对象都是有一个计数器的,当对象被调用一次的时候,该对象的计数器加一,反之则减一,当到最后的时候,该对象计数器
为0则可以判定为该对象没怎么被使用,则被回收避免内存的浪费。

1
2
3
4
String str1 = "This is a String"
String str2 = str1;
String str1 =null;
str2 = new String("This is a String");

当执行到第三行的时候对象仍然被str2使用,所以此时不能被回收,最后一句没有调用str1/str2使用了则可以回收了

类的继承

一个父类有可以有多个子类,一个子类只能有一个直接父类
在java语言中所有类都是直接或间接继承objec类得到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package diyizhang;

class Person {
private String name;
private int age;

public Person() {
System.out.println("调用了个人类的构造方法Person()");
}

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

public void show() {
System.out.println("姓名" + name + "年龄" + age);
}
}

class Student extends Person {
private String department;

public Student() {
System.out.println("调用了学生类的构造方法Student()");
}

public void setDepartment(String dep) {
department = dep;
System.out.println("我是" + department + "的学生");
}

}

public class App8_1 {
public static void main(String[] args) {
Student stu = new Student();
stu.setNameAge("张小三", 21);
stu.show();
stu.setDepartment("计算机系");
}
}
输出结果:
调用了个人类的构造方法Person()
调用了学生类的构造方法Student()
姓名张小三年龄21
我是计算机系的学生

由此可见,本来我们创建的是Student的对象,构造方法来说,应该调用的是Student()构造方法,但是这里调用了父类的构造方法
java语言继承中,执行子类的构造方法之前,会先调用父类中没有参数的构造方法,其目的是要帮助继承自父类的成员做初始化操作

调用父类中特定的构造方法(super)

这里我们知道,在创建Student对象时,默认调用父类的无参构造,那么现在我需要调用有参构造呢?
super关键字是指向该super类的父类

调用父类有参构造,需要在子类的构造函数中进行,然后用super搞定

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

class Person {
private String name;
private int age;


public Person() {
System.out.println("调用了个人类的构造方法Person()");
}

public Person(String name, int age) {
System.out.println("调用了父类的有参构造");
this.name = name;
this.age = age;
}

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

public void show() {
System.out.println("姓名" + name + "年龄" + age);
}
}

class Student extends Person {
private String department;

public Student() {
System.out.println("调用了学生类的构造方法Student()");
}

public Student(String name, int age, String dep) {
super(name, age);
department = dep;
System.out.println("我是" + department + "的学生");
}

}

子类中访问父类的成员

在子类中不仅能访问父类的构造方法,还能访问父类非private的成员变量和成员方法,
但super不能访问在子类中添加的成员
super.变量名
super.方法名()

覆盖

与前面的重载不同的是,重载是指在一个类里面定义多个名称相同但参数个数或类型不同的方法
覆盖则是指在子类中定义名称,参数个数与类型均与父类中完全相同的方法

  • java中提供了一个注解@Override,该注解只用于方法,用来限定必须覆盖父类中的方法
    注意 :子类中不能覆盖父类中声明为final和static的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 父类:动物
class Animal {
// 父类的方法
public void makeSound() {
System.out.println("动物发出声音");
}

public void move() {
System.out.println("动物在移动");
}
}

// 子类:狗(继承自动物)
class Dog extends Animal {
// 覆盖父类的makeSound方法
// 方法名、参数个数和类型与父类完全相同
@Override
public void makeSound() {
System.out.println("狗在汪汪叫");
}

// 这里没有覆盖move方法,将使用父类的实现
}

// 子类:猫(继承自动物)
class Cat extends Animal {
// 覆盖父类的makeSound方法
@Override
public void makeSound() {
System.out.println("猫在喵喵叫");
}

// 同时覆盖父类的move方法
@Override
public void move() {
System.out.println("猫轻轻悄悄地走");
}
}

public class OverrideExample {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog(); // 多态:父类引用指向子类对象
Animal cat = new Cat(); // 多态:父类引用指向子类对象

animal.makeSound(); // 调用父类的方法:动物发出声音
dog.makeSound(); // 调用子类Dog覆盖后的方法:狗在汪汪叫
cat.makeSound(); // 调用子类Cat覆盖后的方法:猫在喵喵叫

animal.move(); // 调用父类的方法:动物在移动
dog.move(); // 调用父类的方法(未被覆盖):动物在移动
cat.move(); // 调用子类Cat覆盖后的方法:猫轻轻悄悄地走
}
}

用父类的对象访问子类的成员

如果希望通过父类类型的引用访问子类成员,需要满足两个条件:

该成员在父类中已定义(即子类覆盖了父类的方法,或继承了父类的属性)
通过多态(父类引用指向子类对象)的方式访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 父类
class Parent {
// 父类的属性
public String parentAttr = "父类属性";

// 父类的方法
public void parentMethod() {
System.out.println("父类方法");
}
}

// 子类
class Child extends Parent {
// 子类独有的属性(父类无法直接访问)
public String childAttr = "子类独有属性";

// 子类覆盖父类的方法
@Override
public void parentMethod() {
System.out.println("子类覆盖的父类方法");
}

// 子类独有的方法(父类无法直接访问)
public void childMethod() {
System.out.println("子类独有方法");
}
}

public class Main {
public static void main(String[] args) {
// 父类对象(只能访问父类自己的成员)
Parent parent = new Parent();
System.out.println(parent.parentAttr); // 正确:访问父类属性
parent.parentMethod(); // 正确:访问父类方法
// System.out.println(parent.childAttr); // 错误:父类对象不能访问子类独有的属性
// parent.childMethod(); // 错误:父类对象不能访问子类独有的方法

// 多态:父类引用指向子类对象
Parent poly = new Child();
System.out.println(poly.parentAttr); // 正确:访问父类继承的属性
poly.parentMethod(); // 正确:访问子类覆盖后的方法(多态特性)
// System.out.println(poly.childAttr); // 错误:父类引用仍不能直接访问子类独有属性
// poly.childMethod(); // 错误:父类引用仍不能直接访问子类独有方法

// 如果必须访问子类独有成员,需要强制类型转换(向下转型)
if (poly instanceof Child) {
Child child = (Child) poly; // 强制转换为子类类型
System.out.println(child.childAttr); // 正确:访问子类独有属性
child.childMethod(); // 正确:访问子类独有方法
}
}
}

final成员与final类

如果一个类或成员被声明为final,则该成员不会被覆盖,则为最终变量

Object

暂存

抽象类

1. 什么是抽象类?

抽象类是用 abstract 关键字修饰的类,它不能被实例化(即不能创建对象)。抽象类通常作为其他类的基类(父类),用于定义公共接口和部分实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public abstract class Animal {
// 类内容
}
2. 为什么需要抽象类?
代码复用:将多个子类的共同代码提取到抽象类中

定义规范:强制子类实现特定的方法

实现多态:通过抽象类引用指向具体子类对象

3. 抽象类的特点
3.1 包含抽象方法
抽象类可以包含抽象方法(没有方法体的方法):

java
public abstract class Animal {
// 抽象方法(没有方法体)
public abstract void makeSound();

// 普通方法(有方法体)
public void sleep() {
System.out.println("动物正在睡觉");
}
}
3.2 不能实例化
java
// 错误:无法实例化抽象类
Animal animal = new Animal(); // 编译错误
3.3 可以包含成员变量
java
public abstract class Animal {
protected String name; // 成员变量
protected int age;

// 构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
3.4 可以包含构造方法
虽然抽象类不能实例化,但可以有构造方法(供子类调用):

java
public abstract class Animal {
private String name;

public Animal(String name) {
this.name = name;
}
}
4. 抽象类的使用
4.1 继承抽象类
子类必须实现父类的所有抽象方法,否则子类也必须声明为抽象类:

java
// 具体子类
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}

// 实现抽象方法
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}

// 使用
Animal myDog = new Dog("Buddy", 3);
myDog.makeSound(); // 输出:汪汪汪!
myDog.sleep(); // 输出:动物正在睡觉
4.2 部分实现抽象方法
如果子类没有实现所有抽象方法,必须声明为抽象类:

java
public abstract class Bird extends Animal {
// 只实现部分抽象方法
@Override
public void makeSound() {
System.out.println("叽叽喳喳");
}

// fly() 方法没有实现,所以Bird仍然是抽象类
public abstract void fly();
}
5. 抽象类 vs 接口
特性 抽象类 接口
方法实现 可以有具体方法和抽象方法 Java 8前只能有抽象方法,之后可以有默认方法
成员变量 可以有各种访问修饰符的变量 默认都是 public static final
构造方法 可以有 不能有
多重继承 单继承 多实现
设计理念 "是什么"(is-a关系) "能做什么"(has-a关系)
6. 实际应用示例
``` java
// 抽象类
public abstract class Employee {
private String name;
private int id;

public Employee(String name, int id) {
this.name = name;
this.id = id;
}

// 抽象方法
public abstract double calculateSalary();

// 具体方法
public String getDetails() {
return "ID: " + id + ", Name: " + name;
}
}

// 具体子类
public class FullTimeEmployee extends Employee {
private double monthlySalary;

public FullTimeEmployee(String name, int id, double monthlySalary) {
super(name, id);
this.monthlySalary = monthlySalary;
}

@Override
public double calculateSalary() {
return monthlySalary;
}
}

// 使用
Employee emp = new FullTimeEmployee(“张三”, 1001, 8000);
System.out.println(emp.getDetails());
System.out.println(“月薪: “ + emp.calculateSalary());
7. 总结
抽象类用 abstract 关键字修饰

不能被实例化,只能被继承

可以包含抽象方法和具体方法

子类必须实现所有抽象方法,否则也必须声明为抽象类

适合用于有共同特征和行为的类的模板设计

抽象类是Java面向对象编程中实现代码重用和多态性的重要工具,常用于框架设计和大型项目的架构中。

接口

  1. 接口是什么?
    定义:接口是一个完全抽象的类型。它是一组行为规范的集合,只声明“应该做什么”,而不关心“如何做”。

核心思想:定义标准,实现解耦。它主要体现的是一种 “像…一样” (can-do) 的关系,而不是“是” (is-a) 的关系。

关键字:interface

  1. 接口的特点 (Java 7及以前)
    在 Java 8 之前,接口是一个非常纯粹的“契约”。

成员变量 (字段):

默认都是 public static final 的常量。

必须显式初始化。

通常用于定义一些全局常量。

1
2
3
4
interface USB {
// 等同于 public static final double VERSION = 3.0;
double VERSION = 3.0;
}

方法:

所有普通方法默认都是 public abstract 的抽象方法(没有方法体)。

没有构造方法,不能被实例化。

1
2
3
4
5
interface Animal {
// 等同于 public abstract void eat();
void eat();
void sleep();
}
  1. 接口的实现
    关键字:implements

规则:

一个类使用 implements 关键字来实现一个或多个接口。

实现类必须重写(Override) 接口中所有的抽象方法,并提供具体的实现。

如果实现类没有重写所有抽象方法,那么它自己必须被声明为 abstract 抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个接口
interface Swimmable {
void swim();
}

// 类实现接口
class Duck implements Swimmable {
// 必须重写接口中的抽象方法
@Override
public void swim() {
System.out.println("鸭子在水上游泳");
}
}
  1. 接口的新特性 (Java 8+)
    Java 8 对接口进行了重大增强,引入了默认方法和静态方法。

4.1 默认方法 (Default Methods)
关键字:default

目的:允许在接口中添加新方法,而不会破坏已有的实现类。

特点:

有方法体,提供了默认实现。

实现类可以直接继承使用这个默认实现,也可以选择重写它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Vehicle {
// 抽象方法
void start();

// 默认方法
default void honk() {
System.out.println("嘀嘀嘀!");
}
}

class Car implements Vehicle {
@Override
public void start() {
System.out.println("汽车启动");
}
// 没有重写 honk(),将使用接口中的默认实现
}

// 使用
Car myCar = new Car();
myCar.start(); // 输出: 汽车启动
myCar.honk(); // 输出: 嘀嘀嘀!

4.2 静态方法 (Static Methods)
关键字:static

目的:将工具方法与接口相关联,避免为工具方法创建额外的类。

特点:

有方法体。

属于接口本身,只能通过接口名直接调用,不能被实现类的对象调用。

1
2
3
4
5
6
7
8
9
10
11
12
interface MathOperation {
static int add(int a, int b) {
return a + b;
}
}

// 使用
int sum = MathOperation.add(5, 3); // 正确:通过接口名调用
System.out.println(sum); // 输出: 8

// MathOperation op = new ...();
// op.add(5, 3); // 错误!不能通过实现类对象调用静态方法

4.3 私有方法 (Private Methods) (Java 9+)
关键字:private

目的:作为默认方法或静态方法的辅助方法,用于抽取公共代码,减少冗余,但又不暴露给外部。

分类:

private:仅供接口内的默认方法使用。

private static:供接口内的默认方法和静态方法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Logger {
default void logInfo(String message) {
log("INFO", message);
}

default void logError(String message) {
log("ERROR", message);
}

// 私有方法,减少重复代码
private void log(String level, String message) {
System.out.println("[" + level + "] " + message);
}
}
  1. 接口的继承
    接口可以使用 extends 关键字继承其他接口。

接口支持多重继承(一个接口可以继承多个父接口),这是与类(单继承)最大的不同之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Animal {
void eat();
}

interface Winged {
void fly();
}

// Bat 接口同时继承了 Animal 和 Winged 接口
interface Bat extends Animal, Winged {
void useEcholocation();
}

// 实现类必须实现所有接口链中的抽象方法
class FruitBat implements Bat {
@Override
public void eat() { /* ... */ }
@Override
public void fly() { /* ... */ }
@Override
public void useEcholocation() { /* ... */ }
}
  1. 接口 vs. 抽象类

特性 接口 (Interface) 抽象类 (Abstract Class)
方法 Java 8前:全是抽象方法
Java 8+:抽象、默认、静态、私有 抽象方法、具体方法都有
变量 只能是 public static final 常量 各类成员变量都可以
构造方法 没有 有(虽然不能直接实例化)
继承方式 多继承(一个类可实现多个接口) 单继承(一个类只能继承一个父类)
设计理念 “has-a” / “can-do”
定义行为契约、标准 “is-a”
表示的是从属关系,代码复用
默认方法 有(Java 8+) 有(一直都是)
访问修饰符 方法默认 public 方法可以是任意访问权限
如何选择?
如果你主要关心对象的 capabilities (能力),而不是其身份 (identity),使用接口。(例如:Comparable, Serializable)
如果你需要定义一种模板,包含一些公共代码,并要求子类是一种更具体的类型,使用抽象类。

下面是接口的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 1. 定义一个「可充电」接口
interface Chargeable {
// 接口常量 (默认 public static final)
String STANDARD = "USB-C";

// 抽象方法 (默认 public abstract)
void charge();

// 默认方法 (Java 8+)
default void showChargingStandard() {
System.out.println("充电标准: " + STANDARD);
}

// 静态方法 (Java 8+)
static void printWelcome() {
System.out.println("欢迎使用充电系统");
}
}

// 2. 手机类实现接口
class Phone implements Chargeable {
private String brand;//品牌
private int batteryLevel;//手机电量

public Phone(String brand) {
this.brand = brand;
this.batteryLevel = 20; // 初始电量20%
}

// 必须实现抽象方法
@Override
public void charge() {
System.out.println(brand + "手机正在充电中...");
batteryLevel += 50;
if (batteryLevel > 100) batteryLevel = 100;
System.out.println("当前电量: " + batteryLevel + "%");
}

// 可以选择重写默认方法
@Override
public void showChargingStandard() {
System.out.println(brand + "手机使用" + STANDARD + "接口充电");
}
}

// 3. 笔记本电脑类实现接口
class Laptop implements Chargeable {
private String model;

public Laptop(String model) {
this.model = model;
}

@Override
public void charge() {
System.out.println(model + "笔记本电脑正在快速充电");
}
// 没有重写showChargingStandard(),将使用接口的默认实现
}

// 4. 主类进行调用
public class InterfaceExample {
public static void main(String[] args) {
// 调用接口的静态方法
Chargeable.printWelcome();
System.out.println("------------------------");

// 创建对象
Phone myPhone = new Phone("华为");
Laptop myLaptop = new Laptop("ThinkPad");

// 使用接口类型引用对象(多态的体现)
Chargeable[] devices = {myPhone, myLaptop};

// 遍历所有设备进行充电
for (Chargeable device : devices) {
System.out.println("--- 开始处理设备 ---");
device.showChargingStandard(); // 调用默认方法
device.charge(); // 调用实现的抽象方法
System.out.println();
}

// 访问接口常量
System.out.println("所有设备都使用" + Chargeable.STANDARD + "标准");
}
}

在这个例子中我发现了一个我不知道的东西

1
2
// 使用接口类型引用对象(多态的体现)
Chargeable[] devices = {myPhone, myLaptop};

深入理解:Chargeable[] devices = {myPhone, myLaptop};

  1. 提高代码的灵活性和可扩展性
    1
    2
    3
    4
    5
    6
    7
    // 假设我们新增一个平板类
    class Tablet implements Chargeable {
    @Override
    public void charge() {
    System.out.println("平板电脑充电中");
    }
    }
    // 原有的代码完全不需要修改!
    Tablet myTablet = new Tablet();
    Chargeable[] devices = {myPhone, myLaptop, myTablet}; // 直接添加新设备
  2. 统一处理不同类型的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 如果没有接口,你需要这样写:
    Phone[] phones = {...};
    Laptop[] laptops = {...};
    Tablet[] tablets = {...};

    // 有了接口,只需要一个循环处理所有设备
    for (Chargeable device : devices) {
    device.charge(); // 统一调用充电方法
    }
  3. 降低代码耦合度
    1
    2
    3
    4
    5
    6
    // 方法参数使用接口类型,可以接受任何实现类
    public void chargeAllDevices(Chargeable[] devices) {
    for (Chargeable device : devices) {
    device.charge();
    }
    }
    // 可以传入任何实现了Chargeable接口的对象数组
    chargeAllDevices(devices);
    底层原理:多态(Polymorphism)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    interface Animal {
    void makeSound();
    }

    class Dog implements Animal {
    @Override
    public void makeSound() {
    System.out.println("汪汪!");
    }

    public void fetch() { // Dog特有的方法
    System.out.println("捡球");
    }
    }

    class Cat implements Animal {
    @Override
    public void makeSound() {
    System.out.println("喵喵!");
    }
    }

    public class PolymorphismExample {
    public static void main(String[] args) {
    // 多态的体现:父类/接口引用指向子类对象
    Animal myAnimal1 = new Dog(); // ✅ 正确
    Animal myAnimal2 = new Cat(); // ✅ 正确

    Animal[] animals = {new Dog(), new Cat()};

    for (Animal animal : animals) {
    animal.makeSound(); // 运行时根据实际对象类型调用相应方法
    }

    // ❌ 编译错误:Animal接口没有fetch()方法
    // myAnimal1.fetch();

    // ✅ 需要先向下转型
    if (myAnimal1 instanceof Dog) {
    Dog myDog = (Dog) myAnimal1;
    myDog.fetch();
    }
    }
    }
    实际应用场景
    场景1:集合框架(最经典的例子)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // List是接口,ArrayList和LinkedList是实现类
    List<String> list1 = new ArrayList<>();
    List<String> list2 = new LinkedList<>();

    // 可以统一处理
    List<String>[] lists = {list1, list2};
    for (List<String> list : lists) {
    list.add("Hello");
    System.out.println(list.size());
    }
    场景2:策略模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    interface PaymentStrategy {
    void pay(double amount);
    }

    class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
    System.out.println("使用信用卡支付: $" + amount);
    }
    }

    class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
    System.out.println("使用PayPal支付: $" + amount);
    }
    }

    class ShoppingCart {
    public void checkout(PaymentStrategy payment, double amount) {
    payment.pay(amount); // 不关心具体实现,只关心行为
    }
    }

    // 使用
    PaymentStrategy[] payments = {
    new CreditCardPayment(),
    new PayPalPayment()
    };

    ShoppingCart cart = new ShoppingCart();
    for (PaymentStrategy payment : payments) {
    cart.checkout(payment, 100.0);
    }
    重要规则总结
    向上转型(Upcasting):接口/父类 引用 = new 实现类/子类() ✅ 总是安全的

向下转型(Downcasting):需要显式转换,且必须用 instanceof 检查 ✅ 谨慎使用

方法调用:编译时看左边(引用类型),运行时看右边(实际对象类型)

字段访问:没有多态,编译和运行时都看左边(引用类型)

接口回调详解:从生活例子到代码实现

什么是接口回调?

这个知识点在之前的学习中用过,但是没有了解过这个叫什么名字重活一次,我一定要搞懂他(重生之我是java高手)
下面是ai给我的讲解

接口回调是一种编程模式,它允许一个类在完成特定任务后通知另一个类。这种模式实现了调用方和被调用方之间的解耦,是事件驱动编程和异步处理的基础。

生活化例子:送衣服干洗

场景分析

想象一下送衣服干洗的完整流程:

  • 你(调用方):需要干洗服务
  • 干洗店(被调用方):提供服务
  • 取衣通知(回调方法):完成后的通知机制

两种处理方式对比

方式一:轮询(低效)

1
2
3
4
5
你:打电话问"衣服好了吗?"
店员:还没
你:(一小时后)"衣服好了吗?"
店员:还没
...(重复直到完成)

缺点:效率低下,浪费资源

方式二:回调(高效)

1
2
3
你:留下电话号码,"洗好后通知我"
干洗店:完成工作后主动打电话通知你
你:在此期间可以做其他事情

优点:高效、解耦、异步处理

代码实现

1. 定义回调接口(协议)

1
2
3
4
5
6
7
8
9
10
/**
* 干洗回调接口 - 定义"通知协议"
*/
public interface DryCleanCallback {
/**
* 衣服准备好时的回调方法
* @param message 通知消息
*/
void onClothesReady(String message);
}

2. 调用方实现回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 客户类 - 调用方,需要被通知
*/
public class Customer implements DryCleanCallback {

private String name;

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

/**
* 实现回调方法:定义收到通知后的具体行为
*/
@Override
public void onClothesReady(String message) {
System.out.println("【客户通知】" + name + " 收到:" + message);
System.out.println(">>> " + name + "的后续动作:准备去取衣服");
// 这里可以添加更复杂的业务逻辑
}

/**
* 送衣服干洗的业务方法
*/
public void sendToDryClean() {
DryCleanShop shop = new DryCleanShop();
System.out.println("🚗 " + name + " 送衣服到干洗店");

// 关键步骤:传递this(即回调接口的实现)
shop.washClothes("冬季大衣", this);

System.out.println("💻 " + name + " 继续做其他事情...");
}
}

3. 被调用方使用回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 干洗店类 - 被调用方,服务提供者
*/
public class DryCleanShop {

/**
* 核心服务方法
* @param clothes 要洗的衣服
* @param callback 回调接口引用(客户的"电话号码")
*/
public void washClothes(String clothes, DryCleanCallback callback) {
System.out.println("\n🏪 干洗店开始工作 =====");
System.out.println("接收衣物:" + clothes);

// 模拟耗时操作
simulateWashingProcess(clothes);

System.out.println("🏪 干洗店工作完成 =====");

// 关键步骤:完成后回调客户
String completionMessage = "您的【" + clothes + "】已清洗消毒完毕,欢迎来取!";
callback.onClothesReady(completionMessage);
}

/**
* 模拟清洗过程
*/
private void simulateWashingProcess(String clothes) {
try {
System.out.println("⏳ 正在清洗 " + clothes + "...");
Thread.sleep(3000); // 模拟3秒清洗时间
System.out.println("✅ " + clothes + " 清洗完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

4. 运行演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 演示类 - 展示完整的回调流程
*/
public class CallbackDemo {
public static void main(String[] args) {
System.out.println("========== 接口回调演示开始 ==========\n");

// 创建客户
Customer customer = new Customer("张三");

// 发送衣服干洗(注册回调)
customer.sendToDryClean();

// 主线程继续执行,演示异步效果
System.out.println("\n📱 主程序继续运行...");
System.out.println("========== 演示结束 ==========");
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
========== 接口回调演示开始 ==========

🚗 张三 送衣服到干洗店

🏪 干洗店开始工作 =====
接收衣物:冬季大衣
⏳ 正在清洗 冬季大衣...
💻 张三 继续做其他事情...

📱 主程序继续运行...
========== 演示结束 ==========
✅ 冬季大衣 清洗完成
🏪 干洗店工作完成 =====
【客户通知】张三 收到:您的【冬季大衣】已清洗消毒完毕,欢迎来取!
>>> 张三的后续动作:准备去取衣服

核心概念解析

1. 解耦 (Decoupling)

  • 干洗店不需要知道具体的客户是谁
  • 客户不需要知道干洗店的具体工作流程
  • 双方只通过接口进行通信

2. 控制反转 (Inversion of Control)

  • 传统:调用方控制整个流程
  • 回调:被调用方在适当时机”回调”调用方
  • 遵循”好莱坞原则”:不要打电话给我们,我们会打电话给你

3. 异步处理 (Asynchronous Processing)

  • 调用方不必阻塞等待
  • 被调用方完成工作后自动通知
  • 提高资源利用率和响应性

实际应用场景

Android开发中的点击事件

1
2
3
4
5
6
7
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 这是回调方法
handleButtonClick();
}
});

JavaScript中的事件处理

1
2
3
4
document.getElementById('myButton').addEventListener('click', function() {
// 回调函数
alert('按钮被点击了!');
});

网络请求回调

1
2
3
4
5
6
7
8
9
10
11
12
13
httpClient.get(url, new HttpResponseCallback() {
@Override
public void onSuccess(String response) {
// 请求成功时的回调
processResponse(response);
}

@Override
public void onError(Exception e) {
// 请求失败时的回调
handleError(e);
}
});

总结

接口回调的三要素

  1. 回调接口:定义通信协议
  2. 调用方:实现接口,提供回调方法的具体逻辑
  3. 被调用方:持有接口引用,在适当时机调用回调方法

核心价值

  • 灵活性:易于扩展和修改
  • 可维护性:代码结构清晰,职责分离
  • 复用性:回调接口可以被多个类实现
  • 异步支持:适合处理耗时操作

记忆技巧

“留个电话,完事儿叫我”

  • 留电话 = 注册回调接口
  • 完事儿 = 被调用方完成工作
  • 叫我 = 执行回调方法

接口回调是现代编程中非常重要的概念,掌握它对于理解事件驱动编程、异步处理和各种设计模式都有很大帮助。

接口的继承

接口也具备继承性,接口继承与类的继承不同的是,接口可以有多个父类,中间用逗号隔开,新接口将继承父接口中的所有常量,抽象方法和默认方法,但不能继承父接口中的静态方法和私有方法
也不能被实现类所继承如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有抽象方法

枚举

java语言中引入了包的概念来管理类名空间。就像文件夹把各种文件进行管理一样。一种区别类名空间的机制

异常处理

异常是指在程序运行中由代码产生的一种错误。

异常处理机制

  1. 抛出异常
    程序的运行中,发生了异常事件,则产生代表该异常的一个异常对象,交给运行系统,由运行系统去找寻对应的代码来处理相关异常
  2. 捕获异常
    异常抛出后,运行系统从生成的异常对象代码开始,沿着方法的调用栈逐层回溯查找,直到找到包含相应处理异常的方法,并把异常对象提交给该方法为止

异常处理类

异常类的最顶层有一个单独的类为:Throwable,该类派生出了Error和Exception两个子类其中Error由系统保留,我们一般使用Excption

Exception异常分类

运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
运行时异常和编译时异常其实很好区分,当你在编码阶段没有报错,而点击运行代码后抛出了错误后这个其实就是运行错误
而编译错误就是在编码阶段就会提醒你的错误,所以编码错误也称检查错误

异常的处理

异常处理是通过try,catch,finally,thow,thows五个关键字实现的

  1. try-catch-finally语法格式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try {
    // 可能会抛出异常的代码
    // 业务逻辑代码
    } catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
    System.out.println("捕获到异常: " + e1.getMessage());
    } catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
    System.out.println("捕获到异常: " + e2.getMessage());
    } finally {
    // 无论是否发生异常都会执行的代码
    // 通常用于资源清理,如关闭文件、数据库连接等
    }
  2. 多异常处理机制:
  • 多异常处理机制:一个try块后可跟多个catch块,分别处理不同类型异常;catch块能捕获父类异常时,也能捕获其所有子类异常。

  • 异常匹配流程try抛异常后,程序先到第一个catch块匹配,匹配即进入该块执行;不匹配则依次往后找,直到找到能接收的catch块。若都不匹配,回退到上层方法找,最终无匹配则由Java运行系统处理(通常终止程序并输出信息)。

  • 无异常情况try内语句无异常时,所有catch块都不执行。

1
2
3
4
5
6
7
8
9
10
try {
test1();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件没有找到");
} catch (ParseException e) {
e.printStackTrace();
System.out.println("时间格式有问题");
}

Java异常中 throw 和 throws 的区别

🎯 核心概念一句话总结

  • **throw**:制造问题(主动扔出一个炸弹)
  • **throws**:提前警告(挂个牌子说”这里有炸弹危险”)

🍎 生活化比喻

场景:你点了一份外卖

throw 就像厨师发现菜有问题时…

1
2
3
4
5
6
7
public void 做菜() {
if(发现食材变质了) {
// 厨师主动抛出问题
throw new 食材变质异常("鸡肉发臭了!");
}
// 正常做菜...
}

厨师说:”这菜我做不了!问题就在这!”

throws 就像菜单上的警告…

1
2
3
public void 做辣子鸡() throws 可能太辣异常, 可能上火异常 {
// 做菜过程...
}

菜单说:”这道菜可能会很辣,吃了可能上火,请您知悉!”


💻 代码示例(一步步来)

第一步:只有 throw(制造问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class 测试 {
public static void 检查年龄(int age) {
if(age < 0) {
// 主动制造一个异常
throw new IllegalArgumentException("年龄不能为负数!");
}
System.out.println("年龄合法: " + age);
}

public static void main(String[] args) {
检查年龄(-5); // 这里会崩溃!程序直接报错停止
}
}

运行结果:程序崩溃,控制台显示 IllegalArgumentException: 年龄不能为负数!

第二步:加上 throws(提前警告)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class 测试 {
// 声明这个方法可能会抛出异常
public static void 检查年龄(int age) throws IllegalArgumentException {
if(age < 0) {
throw new IllegalArgumentException("年龄不能为负数!");
}
System.out.println("年龄合法: " + age);
}

public static void main(String[] args) {
// 现在调用者知道可能有异常,需要处理
try {
检查年龄(-5);
} catch (IllegalArgumentException e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
}

运行结果捕获到异常: 年龄不能为负数!(程序不会崩溃)


📋 最简对比表

特性 throw throws
是什么 动作 - 扔出异常 声明 - 警告可能有问题
在哪里 方法内部 方法开头(签名里)
后面跟什么 new Exception()
(异常对象)
Exception.class
(异常类型)
例子 throw new 炸弹(); throws 可能爆炸警告;

🎯 实际应用场景

场景1:用户注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class 用户服务 {
// 声明:这个方法可能会抛出这些异常
public void 注册用户(String 用户名, String 密码)
throws 用户名已存在异常, 密码太简单异常 {

if(用户已存在(用户名)) {
// 制造:用户名已存在的异常
throw new 用户名已存在异常(用户名 + " 已被注册");
}

if(密码.length() < 6) {
// 制造:密码太简单的异常
throw new 密码太简单异常("密码至少6位");
}

// 正常注册逻辑...
}
}

场景2:调用这个方法时

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
用户服务 service = new 用户服务();

try {
service.注册用户("张三", "123");
System.out.println("注册成功!");
} catch (用户名已存在异常 e) {
System.out.println("注册失败: " + e.getMessage());
} catch (密码太简单异常 e) {
System.out.println("注册失败: " + e.getMessage());
}
}

❓ 常见疑问解答

问:为什么要用 throws?直接 throw 不行吗?

答:throws 是给调用者的温馨提示

  • 没有 throws:调用者不知道可能出什么错,突然就崩溃了
  • throws:调用者知道可能出哪些错,可以提前准备处理方案

问:什么时候必须用 throws

答:当你抛出检查型异常时(比如 IOException, SQLException),Java强制要求你必须声明 throws。这是Java的安全机制。


💡 总结

可以把 throw 想象成 扔手榴弹throws 想象成 贴警告标志

  • throw:负责制造危险
  • throws:负责提前告知危险

java语言的输入输出与文件处理

文件的创建方法

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

public class creatFile {
public void fileCreat1() {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\fileTest1.txt";
File file = new File(filePath);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
e.printStackTrace();
}
}

//方式2 new File(File parent,String child)//根据父目录文件+子路径构建//e:llnews2.txt
public void create02() {
File parentFile = new File("D:\\JAVA\\newJava\\new1\\src\\IO\\File");
String fileName = "news2.txt";
File file = new File(parentFile, fileName);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
throw new RuntimeException(e);
}

}

////方式3 new File(string parent,string child)//根据父目录+子路径构建
public void create03() {
String parentFile = "D:\\JAVA\\newJava\\new1\\src\\IO\\File";
String child = "news3.txt";
File file = new File(parentFile, child);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
creatFile creatFile = new creatFile();
creatFile.fileCreat1();
creatFile.create02();
creatFile.create03();
}

}

获取文件信息

  • 获取文件的信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import java.io.File;

    public class FileInformation {
    public static void main(String[] args) {
    info();
    }

    public static void info() {
    File file = new File("D:\\JAVA\\newJava\\new1\\src\\IO\\File\\fileTest1.txt");
    System.out.println("文件的名字是:" + file.getName());
    /*
    getAbsolutePath() - 获取文件的绝对路径
    getParent() - 获取父目录路径
    length() - 获取文件大小(字节数)
    exists() - 判断文件/目录是否存在
    isFile() - 判断是否为文件
    isDirectory() - 判断是否为目录*/
    System.out.println(file.getAbsolutePath());
    System.out.println(file.getParent());
    System.out.println(file.length());
    System.out.println(file.exists());
    System.out.println(file.isFile());
    System.out.println(file.isDirectory());
    }
    }

目录的操作

在java编程中,目录也被当做文件
创建一级目录使用mkdir(),创建多级目录使用mkdirs()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.File;

public class Directory_s {
public static void main(String[] args) {
Directory_s directoryS = new Directory_s();
directoryS.m1();
directoryS.m2();
}

public void m1() {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\news2.txt";
File file = new File(filePath);
if (file.exists()) {
System.out.println("文件存在");
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败");
}
} else {
System.out.println("该文件不存在");
}
}

//这里我们需要体会到,在java编程中,目录也被当做文件(使用的时候就当文件那样删除和新增就行了)
public void m2() {
String directoryPath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02";
File file = new File(directoryPath);
if (file.exists()) {
System.out.println("文件存在");

} else {
System.out.println("文件不存在");
if (file.mkdir()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件创建失败");
}

}
}

}

I0流原理及流的分类

I0流原理

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

流的分类

流的分类
按操作数据单位不同分为:字节流(8 bit),字符流(按字符) 效率高一些
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流

IO流体系图-常用的类

InputStream:字节输入流
InputStream 抽象类是所有字节输入流的超类。

InputStream 常用的子类:

FileInputStream:文件输入流

BufferedInputStream:缓冲字节输入流

ObjectInputStream:对象字节输入流

FileInputStream


这里讲了两种写法,第一种是循环读取一个一个的读取字节数并且转成char类型
这里我觉得IO流这边很多异常处理的地方,所以我统一向上抛了

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

//单个读取效率不高
//字节输入流,文件到程序
public class FileInputstream extends Exception {
public static void main(String[] args) throws IOException {
FileInputstream fileInputstream = new FileInputstream();
fileInputstream.fileRead01();
}

//这里注意是大写Stream
public void fileRead01() throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
int read = 0;

//这里注意是大写Stream
FileInputStream fileInputstream = new FileInputStream(filePath);
//从该输入流读取一个字节的数据。如果没有输入可用,此方法将阻止。
//如何返回-1,表示读取完毕

while ((read = fileInputstream.read()) != -1) {
System.out.println((char) read);//读取的时候是int 类型的,展示的时候要转为char类型的
}
fileInputstream.close();//当文件完成读取以后需要将其关闭
}
}

这里是用数组的形式读取,其实就是8个8个的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileInputStream;
import java.io.IOException;

//优化版本的用数组的形式读取文件
//字节输入流,文件到程序
public class FileInputstream_1 extends Exception {
public static void main(String[] args) throws IOException {
FileInputstream_1 fileInputstream = new FileInputstream_1();
fileInputstream.fileRead01();
}

//这里注意是大写Stream
public void fileRead01() throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
// int read = 0;
byte[] buf = new byte[8];
int readLenth = 0;//读取的字节长度
//这里注意是大写Stream
FileInputStream fileInputstream = new FileInputStream(filePath);
//从该输入流读取一个字节的数据。如果没有输入可用,此方法将阻止。
//如何返回-1,表示读取完毕
//如果读取正常,返回实际读取的字节数
while ((readLenth = fileInputstream.read(buf)) != -1) {//改这里
System.out.println(new String(buf, 0, readLenth));//读取的时候是int 类型的,展示的时候要转为char类型的
System.out.println(readLenth);
}
fileInputstream.close();//当文件完成读取以后需要将其关闭
}
}

FileOutputStream


其实跟输入流大差不差的,就是将数据写入到文件中

  1. 同样的是先测试一个字节的写入,同时注意,这里是直接覆盖了文件原有的内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.io.FileOutputStream;
    import java.io.IOException;

    public class FileOutputStream01 {
    public static void main(String[] args) throws IOException {
    writeFile();
    }

    public static void writeFile() throws IOException {
    String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath);
    //写一个字节,会覆盖之前文件中的内容
    fileOutputStream.write('a');
    fileOutputStream.close();//关闭
    }
    }

  2. 写入多个内容的时候,这里用字符串输入,然后用字符串自带的转byte的功能转换就行了(同样也是覆盖原有的内容)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.Scanner;

    public class FileOutputStream02 {
    public static void main(String[] args) throws IOException {
    writeFile();
    }

    public static void writeFile() throws IOException {
    String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath);
    //写多个字节,String 自带转为bytes的功能,我们就懒得转换了
    String str = "hello word";
    fileOutputStream.write(str.getBytes());
    fileOutputStream.close();//关闭
    }
    }

  3. 要想不覆盖原有的内容也很简单
    1
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath,true);//也就是追加的方式写入

文件拷贝

完成 文件拷贝 将
思路分析
先从文件的输入流,到java中去,然后再从java中输出文件文件很大循环
完成程序时,应该是读取部份数据,就写入到指定位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopy {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\BufferedInputStream.png";
String filePathDest = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02\\BufferedInputStream.png";
int readLen = 0;
FileInputStream fileInputStream = new FileInputStream(filePath);
FileOutputStream fileOutputStream = new FileOutputStream(filePathDest);
byte[] buf = new byte[8];//这个写的8个效率太低,用1024,
while ((readLen = fileInputStream.read(buf)) != -1) {
//读取到后,就写入到文件
//即,边读边写
fileOutputStream.write(buf, 0, readLen);
}
System.out.println("拷贝成功");
fileInputStream.close();
fileOutputStream.close();
}

}
  • 注意:fileInputStream.read() 不带参数时,返回的是单个字节(0-255),不是读取的字节数
    当 readLen 大于缓冲区大小时(8),就会抛出 IndexOutOfBoundsException

常用的类

  • FileReader 和 FileWriter 介绍
    FileReader 和 FileWriter 是 Java 中用于字符流操作的类,专门用于读写文本文件。

  • 基本概念
    FileReader
    用于从文件读取字符数据

继承自 InputStreamReader

使用系统默认字符编码

  • FileWriter
    用于向文件写入字符数据

继承自 OutputStreamWriter

使用系统默认字符编码

FileWriter 常用方法

📝 构造函数

方法 说明
new FileWriter(File/String) 覆盖模式,流指针在文件开头,会覆盖原有内容
new FileWriter(File/String, true) 追加模式,流指针在文件末尾,在原有内容后追加

✍️ 写入方法

方法 说明 示例
write(int c) 写入单个字符 writer.write('A');
write(char[] cbuf) 写入整个字符数组 writer.write(charArray);
write(char[] cbuf, int off, int len) 写入字符数组的指定部分 writer.write(chars, 0, 5);
write(String str) 写入整个字符串 writer.write("Hello");
write(String str, int off, int len) 写入字符串的指定部分 writer.write("Hello", 1, 3);

🔗 相关 API

String 类方法:

  • toCharArray() - 将 String 转换成 char[]

💡 重要注意事项

FileWriter 使用后,必须要关闭 (close) 或刷新 (flush),否则写入不到指定的文件!

📋 完整示例代码

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

public class FileWriterDemo {
public static void main(String[] args) {
// 1. 覆盖模式示例
try (FileWriter writer1 = new FileWriter("output.txt")) {
writer1.write("这是第一行 - 覆盖模式\n");
writer1.write("原有内容会被覆盖");
} catch (IOException e) {
e.printStackTrace();
}

// 2. 追加模式示例
try (FileWriter writer2 = new FileWriter("output.txt", true)) {
writer2.write("\n这是追加的内容 - 追加模式");
} catch (IOException e) {
e.printStackTrace();
}

// 3. 各种写入方法示例
try (FileWriter writer3 = new FileWriter("demo.txt")) {
// 写入单个字符
writer3.write(65); // 写入 'A'
writer3.write('\n');

// 写入字符数组
char[] chars = {'H', 'e', 'l', 'l', 'o'};
writer3.write(chars);
writer3.write('\n');

// 写入字符数组的指定部分
writer3.write(chars, 1, 3); // 写入 "ell"
writer3.write('\n');

// 写入整个字符串
String text = "Hello World";
writer3.write(text);
writer3.write('\n');

// 写入字符串的指定部分
writer3.write(text, 6, 5); // 写入 "World"
writer3.write('\n');

// 使用 String.toCharArray()
char[] textArray = text.toCharArray();
writer3.write(textArray);

} catch (IOException e) {
e.printStackTrace();
}
}
}

🎯 使用要点

1. 资源管理

1
2
3
4
5
6
7
8
9
10
11
12
// 推荐:使用 try-with-resources 自动关闭
try (FileWriter writer = new FileWriter("file.txt")) {
writer.write("内容");
} // 自动调用 close()

// 或者手动关闭
FileWriter writer = new FileWriter("file.txt");
try {
writer.write("内容");
} finally {
writer.close(); // 必须关闭!
}

2. 刷新缓冲区

1
2
3
4
5
FileWriter writer = new FileWriter("file.txt");
writer.write("内容");
writer.flush(); // 立即将缓冲区内容写入文件
// ... 其他操作
writer.close();

3. 模式选择

1
2
3
4
5
// 覆盖模式 - 文件存在则清空重写
FileWriter writer1 = new FileWriter("data.txt");

// 追加模式 - 在文件末尾添加内容
FileWriter writer2 = new FileWriter("data.txt", true);

⚠️ 常见错误

  1. 忘记关闭或刷新流

    1
    2
    3
    4
    // 错误:内容可能不会写入文件
    FileWriter writer = new FileWriter("file.txt");
    writer.write("内容");
    // 缺少 close() 或 flush()
  2. 使用错误的模式

    1
    2
    // 如果不想覆盖原有内容,记得使用追加模式
    FileWriter writer = new FileWriter("log.txt", true);

✅ 最佳实践

  1. 始终使用 try-with-resources
  2. 根据需求选择合适的模式(覆盖/追加)
  3. 及时关闭或刷新流
  4. 处理 IOException 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileWriter;
import java.io.IOException;

public class FileWriter_ {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//这个是覆盖
FileWriter fileWriter = new FileWriter(filePath);
//写入单个字符 write(int)
fileWriter.write('H');
//写入指定数组write(char[])
char[] chars = {'a', 'b', 'c'};
fileWriter.write(chars);
//write(char[],off,len):写入指定数组的指定部分
fileWriter.write("成都东软学院".toCharArray(), 0, 3);
//写入整个字符串write(String)
fileWriter.write("你好北京");
fileWriter.close();//一定要关闭,才能写入。如果不关闭文件会创建但是不会写入代码
//在数据量大的情况下使用循环操作
}

}

节点流和处理流

  1. 节点流可以从一个特定的数据源读写数据,如FileReader、FileWriter
  2. 处理流(也叫包装流)是“连接”在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,如BufferedReader、BufferedWriter

📚 基本概念

节点流 (Node Stream)

  • 定义:直接从特定数据源读写数据的流
  • 特点:数据源的直接连接点
  • 比喻:直接的水龙头

处理流 (Processing Stream) / 包装流 (Wrapper Stream)

  • 定义:连接在已有流之上,提供增强功能的流
  • 特点:不直接连接数据源,而是包装其他流
  • 比喻:水龙头上的过滤器或增压器

🆚 对比表格

特性 节点流 处理流
数据源连接 直接连接 间接连接(通过包装其他流)
功能 基础读写操作 增强功能(缓冲、转换等)
独立性 可独立使用 必须依赖其他流
性能 相对较低 通常更高(通过缓冲等机制)
例子 FileReader, FileWriter BufferedReader, BufferedWriter

💡 核心理解

设计模式:装饰器模式

处理流基于装饰器模式设计,允许动态地为对象添加功能。

流的关系

1
数据源 ← 节点流 ← 处理流 ← 处理流 ← 程序

📝 代码示例

1. 纯节点流使用(低效方式)

1
2
3
4
5
6
7
// 类比:直接对着水龙头喝水
FileReader fileReader = new FileReader("test.txt");
int data;
while ((data = fileReader.read()) != -1) {
System.out.print((char) data); // 一次读一个字符,效率低
}
fileReader.close();

2. 节点流 + 处理流(推荐方式)

1
2
3
4
5
6
7
8
9
10
// 类比:水龙头 + 水桶,一次接很多水
FileReader fileReader = new FileReader("test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);

String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line); // 一次读一行,效率高!
}

bufferedReader.close(); // 关闭处理流会自动关闭节点流

3. 多层处理流包装

1
2
3
4
5
6
7
8
9
10
11
12
// 文件 → 文件流 → 缓冲流 → 数据流
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.dat")
)
);

// 使用增强功能
int number = dis.readInt();
double value = dis.readDouble();

dis.close();

🛠️ 常见流分类

节点流示例

数据源 输入流 输出流
文件 FileInputStream
FileReader
FileOutputStream
FileWriter
内存数组 ByteArrayInputStream
CharArrayReader
ByteArrayOutputStream
CharArrayWriter
管道 PipedInputStream
PipedReader
PipedOutputStream
PipedWriter

处理流示例

功能 输入流 输出流
缓冲 BufferedInputStream
BufferedReader
BufferedOutputStream
BufferedWriter
数据类型 DataInputStream DataOutputStream
对象序列化 ObjectInputStream ObjectOutputStream
转换 InputStreamReader OutputStreamWriter

✅ 最佳实践

1. 正确的关闭方式

1
2
3
4
5
6
7
// 推荐:使用try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理数据
}
} // 自动关闭所有流

2. 流组合原则

1
2
3
4
5
6
// 良好的流组合
BufferedReader br = new BufferedReader(
new InputStreamReader( // 字节转字符
new FileInputStream("file.txt") // 节点流
)
);

3. 资源管理

  • 只需要关闭最外层的处理流
  • 内层流会自动关闭
  • 推荐使用try-with-resources确保资源释放

🎯 核心要点总结

  1. 节点流是基础:建立与数据源的直接连接
  2. 处理流是增强:提供缓冲、转换、数据类型处理等高级功能
  3. 灵活组合:可以根据需要多层包装处理流
  4. 性能优化:处理流通常通过缓冲机制提升I/O性能
  5. 资源管理:正确处理流的关闭顺序和异常情况

BufferedReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReader_01 {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//创建BufferedReader对象
BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
String line;//读取一行的信息
//当字符串返回null的时候就表述读取完毕
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
//关闭流
bufferedReader.close();
}
}

BufferedWrite

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWrite_ {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
bufferedWriter.write("晚上好,我在上课,看到信息后给你回复(自动回复 留言模式休眠30秒)");
bufferedWriter.close();
}
}

用BufferedWrite和BufferedRead进行文本文件的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;

public class BufferedCopy_ {
public static void main(String[] args) throws IOException {
String scrFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
String mudi = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\writer\\story.txt";
String line;
//1.BufferedReader和BufferedWriter 是安装字符操作
//2.不要去操作 二进制文件,文件可能会损坏
BufferedReader bufferedReader = new BufferedReader(new FileReader(scrFilePath));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(mudi));
while ((line = bufferedReader.readLine()) != null) {
bufferedWriter.write(line);
bufferedWriter.newLine();
}
bufferedWriter.close();
bufferedReader.close();
}
}

用BufferedInputStream和BufferedOutputStream进行音频视频图片的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;

public class BufferedCopy_2 {
public static void main(String[] args) throws IOException {
String srcFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02\\BufferedInputStream.png";
String mudi = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\writer\\copy.png";
byte[] buf = new byte[1024];
int readLen;
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(srcFilePath));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(mudi));
while ((readLen = bufferedInputStream.read(buf)) != -1) {
bufferedOutputStream.write(buf, 0, readLen);
}
bufferedInputStream.close();
bufferedOutputStream.close();


}
}
方法规律
read(byte[]):字节流读取,返回读取的字节数

readLine():字符流读取一行,返回String

write(byte[], 0, len):字节流写入,要指定长度

write(String):字符流直接写字符串

节点流和处理流 - 对象流

需求场景

  1. int num = 100 这个 int 数据保存到文件中,注意不是保存数字”100”,而是保存 int 类型的 100,并且能够从文件中直接恢复 int 100
  2. Dog dog = new Dog("小黄", 3) 这个 dog 对象保存到文件中,并且能够从文件恢复

序列化和反序列化

序列化 (Serialization)

  • 在保存数据时,保存数据的数据类型
  • 将对象转换为字节序列的过程

反序列化 (Deserialization)

  • 在恢复数据时,恢复数据的数据类型
  • 将字节序列恢复为对象的过程

对象流类

ObjectOutputStream

  • 序列化功能,将对象转换为字节流写入文件

ObjectInputStream

  • 反序列化功能,从文件中读取字节流并恢复为对象

实现序列化的条件

要让某个对象支持序列化机制,其类必须是可序列化的。必须实现以下接口之一:

Serializable 接口

  • 这是一个标记接口,没有需要实现的方法
  • 推荐使用,比较简单

Externalizable 接口

  • 该接口有方法需要实现
  • 一般使用上面的 Serializable 接口

示例代码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 需要序列化的类必须实现 Serializable
class Dog implements Serializable {
private String name;
private int age;

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

// getters and setters
}

// 序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.dat"));
oos.writeInt(100); // 序列化基本数据类型
oos.writeObject(dog); // 序列化对象

// 反序列化操作
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.dat"));
int num = ois.readInt(); // 反序列化基本数据类型
Dog restoredDog = (Dog) ois.readObject(); // 反序列化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;

public class ObjectOutStream_ {
public static void main(String[] args) throws IOException {
String sFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\FileOutputStream\\data.dat";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(sFilePath));
//序列化数据到文件
objectOutputStream.write(100);//int ->Integer(实现了Serializable)
objectOutputStream.writeBoolean(true);//boolean -> Boolean(实现了Serializable)
objectOutputStream.write('a');//char ->Character(实现了Serializable)
objectOutputStream.writeDouble(9.5);//double ->Double(实现了Serializable)
objectOutputStream.writeUTF("字符串形式");//String

//狗狗对象序列化
objectOutputStream.writeObject(new Dog("旺财"));
//这里注意,对象里面的序列化了但是对象本身没有序列化对象实现一个Serializable就行了

}
}

class Dog implements Serializable {
String name;

public Dog(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class ObjectInputStream_ {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//读取文件的路径
String sFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\FileOutputStream\\data.dat";
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(sFilePath));
//1.读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致
System.out.println(objectInputStream.readInt());
System.out.println(objectInputStream.readBoolean());
System.out.println(objectInputStream.read());
System.out.println(objectInputStream.readDouble());
System.out.println(objectInputStream.readUTF());
Object dog = objectInputStream.readObject();
System.out.println("运行的类型:" + dog.getClass());
System.out.println("dog信息:" + dog);//dog信息:IO.File.FileOutputStream.Dog@1a93a7ca的原因是没有在dog里面重写一下
//注意重写了对象里面的内容,需要重新执行序列化一次才能正确的反序列化,否则反序列化报错
objectInputStream.close();
//1。如果我们希望调用Dog的方法,需要向下转型
// 2。需要我们将Dog类的定义,拷贝到可以引用的位置
Dog dog1 = (Dog) dog;
dog1.getName();
}
}

class Dog implements Serializable {
String name;

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

public String getName() {
return name;
}

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

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

节点流与处理流核心区别及序列化注意事项

本文将清晰梳理节点流与处理流的核心差异,并系统整理序列化与反序列化操作的关键注意事项。

一、节点流与处理流核心区别

节点流和处理流的本质区别在于是否直接连接数据源/目的地,以及功能定位的不同。

对比维度 节点流(Node Stream) 处理流(Processing Stream)
数据源连接 直接连接数据源或目的地 不直接连接,而是包裹节点流或其他处理流
功能定位 负责基础的读写数据操作,是IO操作的基础 对已有流的数据进行加工处理,如缓冲、转换、过滤等
依赖关系 可独立使用,无需依赖其他流 必须依赖节点流或其他处理流才能工作
典型示例 FileInputStream、FileOutputStream、FileReader BufferedInputStream、BufferedReader、ObjectOutputStream

二、序列化与反序列化注意事项

在使用处理流(如ObjectOutputStream/ObjectInputStream)进行对象序列化或反序列化时,需严格遵循以下规则。

  1. 读写顺序必须一致
    序列化时写入对象的顺序,与反序列化时读取对象的顺序必须完全相同,否则会抛出EOFExceptionClassCastException

  2. 类必须实现Serializable接口
    只有实现了java.io.Serializable接口的类,其对象才能被序列化。该接口为标记接口,不含任何抽象方法,仅用于标识类具备序列化能力。

  3. 建议显式添加SerialVersionUID
    显式声明private static final long serialVersionUID,可固定类的版本标识。若不添加,JVM会根据类结构自动生成,类结构(如属性、方法)修改后会生成新值,导致旧版本序列化文件无法反序列化,影响版本兼容性。

  4. static与transient修饰的成员不序列化
    序列化时,默认对对象所有非静态、非瞬态属性进行序列化;static修饰的静态属性属于类,transient修饰的瞬态属性为临时数据,两者均不会被序列化。

  5. 属性类型需同样实现Serializable
    若对象的某个属性为自定义类型,该自定义类型也必须实现Serializable接口,否则序列化时会抛出NotSerializableException

  6. 序列化具备可继承性
    若父类已实现Serializable接口,其所有子类会默认继承该能力,无需再显式实现Serializable接口,子类对象可直接参与序列化。


标准输入流输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.BufferedInputStream;

public class InputAndoutput {
public static void main(String[] args) {
//System 类public final static InputStream in = null;
//System.in 编译类型InputStream
//System.in 运行类型BufferedInputStream
//System.in 标准输入 键盘
System.out.println(System.in.getClass());
//public static final PrintStream out = null;
//System.out表示标准输出 显示器
System.out.println(System.out.getClass());
}
}

乱码引出转换流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//读取文件

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

//演示使用 InputStreamReader 转换流解决中文乱码问题
//将字节流 FileInputstream 转成字符流,指定编码 gbk/utf-8
public class InputStreamReader_ {
public static void main(String[] args) throws IOException {
//地址
String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//创建
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(FilePath), "gbk");
//把inputStreamReader传入buff
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = bufferedReader.readLine();
System.out.println("输出的信息:" + s);
//关闭流,关闭外层的
bufferedReader.close();


}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
保存文件
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class QutputStreamWriter_ {
public static void main(String[] args) throws IOException {
//路径
String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(FilePath), "gbk"));
bufferedWriter.write("hello hi");
bufferedWriter.close();
}
}

PrintStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.IOException;
import java.io.PrintStream;

public class PrintStream_ {
public static void main(String[] args) throws IOException {
PrintStream out = System.out;
//底层用的write
out.print("输出相关的内容");
//还可以直接调用write进行打印输出
out.write("调用输出".getBytes());


//可以修改打印流的位置
//我们可以大修改打印流输出的位置/设备
// 修政成到("e:\\f1.txt")
System.setOut(new PrintStream("D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt"));
System.out.println("再次输出相关内容");
out.close();
}
}

PrintWriter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

public class PrintWriter_ {
public static void main(String[] args) throws IOException {
// PrintWriter printWriter = new PrintWriter(System.out);
// printWriter.write("输出相关内容");
//现在是输出文件内容
PrintWriter writer = new PrintWriter(new FileWriter("D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt"));
writer.print("输出相关内容");
writer.close();

}
}

泛型与集合

泛型

这种情况可以是可以,但是一旦添加其他的类型,比如添加猫的类型进去,系统不会检查到错误
而是运行后才会报错。通过使用

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

public class generic_01 {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add(new Dog("1", 2, "白色"));
arrayList.add(new Dog("2", 3, "黑色"));
arrayList.add(new Dog("3", 4, "蓝色"));
for (Object o : arrayList
) {
//向下转型
Dog dog = (Dog) o;
System.out.println(dog.getName() + dog.getAge() + dog.getColor());
}
}


}

class Dog {
String name;
int age;
String color;

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public String getColor() {
return color;
}
}

改进后的:这样就限制了传入参数的类型,同时遍历的时候就不用进行向下转型了

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

import java.util.ArrayList;

public class generic_02 {
public static void main(String[] args) {
ArrayList<Dog> arrayList = new ArrayList<Dog>();
arrayList.add(new Dog("1", 2));
arrayList.add(new Dog("3", 3));
arrayList.add(new Dog("4", 4));
for (Dog dog : arrayList) {
System.out.println(dog.getName() + dog.getAge());
}
}
}

class Dog {
private String name;
private int age;

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

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

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

###泛型的介绍
(1)泛型又称参数化类型,是Jdk5.0 出现的新特性,解决数据类型的安全性问题
(2)在类声明或实例化时只要指定好需要的具体的类型即可。
(3)Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型或者是某个方
(4)法的返回值的类型,或者是参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,
//或者是某个方法的返回值的类型,或者是参数类型
public class generic_03 {
public static void main(String[] args) {
person<String> stringperson = new person<String>("nihao");
System.out.println(stringperson.getS());
}
}

class person<E> {
E s;

public person(E s) {
this.s = s;
}

public E f() {
return s;
}

public E getS() {
return s;
}

public void setS(E s) {
this.s = s;
}
}

泛型的语法

  1. 泛型的声明:
    interface 接□{} 和 class 类<K,V>{}
    //比如:List , ArrayList
    (1)其中,T,K,V不代表值,而是表示类型,
    (2)任意字母都可以。常用T表示,是Type的缩写
  2. 泛型的实例化:
    要在类名后面指定类型参数的值(类型)。如:
    (1)ListstrList = new ArrayList();
    (2)lteratoriterator = customers.iterator();

细节一

  • 泛型中的类型参数(如T、E等)只能是引用类型。
  • 示例说明:
    • List<Integer> list = new ArrayList<Integer>(); // 正确
    • List<int> list2 = new ArrayList<int>(); // 错误
  • 在给泛型指定具体类型后,可以传入该类型或者其子类类型。
  • 泛型使用形式:
    • List<Integer> list1 = new ArrayList<Integer>();
    • List<Integer> list2 = new ArrayList<>();
  • 说明:如果写成 List list3 = new ArrayList();,默认的泛型是 <E>,其中E为Object。
    List list = new ArrayList(); 这种默认位object类型

自定义泛型

以下是关于自定义泛型类的整理,包含基本语法和注意细节:

基本语法

1
2
3
class 类名<T, R, ...> {  // T、R 为泛型参数,可多个,用逗号分隔
// 类成员(属性、方法等)
}

注意细节

  1. 普通成员可使用泛型
    类中的属性、方法(非静态)可以使用类声明的泛型参数。
    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class GenericClass<T> {
    private T value; // 泛型属性

    public T getValue() { // 泛型方法返回值
    return value;
    }

    public void setValue(T value) { // 泛型方法参数
    this.value = value;
    }
    }
  2. 使用泛型的数组不能直接初始化
    泛型数组在声明时可以使用泛型,但不能直接创建泛型数组的实例(因泛型类型在编译时擦除,无法确定具体类型)。
    示例:

    1
    2
    3
    4
    class GenericClass<T> {
    private T[] arr; // 允许声明泛型数组引用
    // private T[] arr = new T[10]; // 错误!不能直接初始化
    }
  3. 静态方法中不能使用类的泛型
    静态成员(方法/属性)属于类级别,而泛型类的类型参数在创建对象时才确定,两者生命周期不匹配。
    示例:

    1
    2
    3
    4
    class GenericClass<T> {
    // public static T staticValue; // 错误!静态属性不能用类泛型
    // public static T getStaticValue() { return null; } // 错误!静态方法不能用类泛型
    }
  4. 泛型类的类型在创建对象时确定
    创建泛型类对象时,必须指定具体的类型(除非使用菱形语法省略,编译器会自动推断)。
    示例:

    1
    2
    GenericClass<String> strObj = new GenericClass<>();  // 确定类型为 String
    GenericClass<Integer> intObj = new GenericClass<>(); // 确定类型为 Integer
  5. 未指定类型时默认为 Object
    若创建对象时不指定泛型类型,泛型参数会被擦除为 Object,失去泛型的类型约束作用。
    示例:

    1
    2
    3
    GenericClass obj = new GenericClass();  // 等价于 GenericClass<Object>
    obj.setValue("test"); // 允许,因 Object 可接收任意类型
    Object value = obj.getValue(); // 返回值为 Object 类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package generic.coustemGeneric;

public class coustemGeneric_ {
public static void main(String[] args) {

}
}

//Tiger后面泛型,我们把Tiger就称为自定义泛型类
//泛型标识符可以有多个

class Tiger<T, R, M> {
String name;
R r;//属性使用到泛型了
T m;
T t;

// T[] ts = new T[8];//不能实例化,因为类型没有确定下来,无法知道开辟多大的空间
//静态方法不能使用泛型
/* public static void m1(M m) {
}
静态是和类相关的,可能在类加载时,对象还没有创建,所以如果静态方法和静态属性使用了泛型,jvm就无法
完成初始化
*/

public Tiger(String name, R r, T m, T t) {//构造器使用泛型
this.name = name;
this.r = r;
this.m = m;
this.t = t;
}

//方法使用到泛型
public String getName() {
return name;
}

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

public R getR() {
return r;
//返回类型可以使用到泛型
}

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

public T getM() {
return m;
}

public void setM(T m) {
this.m = m;
}

public T getT() {
return t;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 声明泛型类型为 <Double, String, Integer> 的对象g
Tiger<Double, String, Integer> g = new Tiger<>("john");

// 2. 调用setT方法(T的类型已确定为Double)
g.setT(10.9); // 正确:10.9是Double类型(自动装箱)
// g.setT("yy"); // 错误:"yy"是String,与T的类型Double不匹配

System.out.println(g); // 输出:Tiger{name='john', t=10.9}

// 3. 未指定泛型类型的对象g2(默认泛型为Object)
Tiger g2 = new Tiger("john~~"); // 等价于 Tiger<Object, Object, Object>
g2.setT("yy"); // 正确:T默认为Object,"yy"是Object的子类

System.out.println("g2=" + g2); // 输出:g2=Tiger{name='john~~', t=yy}

自定义泛型接口

基本语法

1
2
3
interface 接口名 <T,R...>{
}

  • 注意细节
    接口中,静态成员不能使用泛型(和前面的一样的道理)
  • 泛型接口的类型在继承接口或者实现接口时确定
  • 没有指定类型,默认为Object

自定义泛型方法

基本语法

1
2
3
修饰符 <T, R, ...> 返回类型 方法名(参数列表) {
// 方法体(可使用泛型参数T、R等)
}
  • <T, R, ...> 是泛型方法的标志,必须放在修饰符与返回类型之间,声明该方法的泛型参数。

注意细节

  1. 泛型方法的定义位置
    泛型方法可以定义在普通类中,也可以定义在泛型类中。
    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 普通类中的泛型方法
    class NormalClass {
    public <T> T getValue(T t) {
    return t;
    }
    }

    // 泛型类中的泛型方法(泛型参数可与类泛型不同)
    class GenericClass<E> {
    public <T> void print(T t) { // T是方法自身的泛型,与类的E无关
    System.out.println(t);
    }
    }
  2. 泛型方法的类型确定时机
    泛型方法的具体类型在调用时确定(编译器根据传入的参数类型自动推断,或显式指定)。
    示例:

    1
    2
    3
    NormalClass nc = new NormalClass();
    String str = nc.getValue("test"); // 调用时确定T为String
    Integer num = nc.getValue(123); // 调用时确定T为Integer
  3. 区分“泛型方法”与“使用泛型的方法”

    • 若方法没有<T, R...>声明,即使使用了泛型参数(如类的泛型),也不是泛型方法,只是“使用了泛型的方法”。
      示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class GenericClass<E> {
      // 这不是泛型方法,只是使用了类的泛型参数E
      public void eat(E e) {
      System.out.println(e);
      }

      // 这是泛型方法(有<T>声明)
      public <T> void drink(T t) {
      System.out.println(t);
      }
      }
      区别:泛型方法的泛型参数独立于类的泛型,而“使用泛型的方法”依赖于类的泛型类型。

泛型的继承和通配符

1. 泛型不具备继承性

泛型类型之间没有继承关系,即使它们的原始类型有继承关系。
示例:

1
2
// 错误!泛型不具备继承性
List<Object> list = new ArrayList<String>(); // 编译报错

原因:ArrayList<String> 不是 List<Object> 的子类,泛型的类型参数会在编译时进行严格检查,避免类型不安全的操作。

2. 泛型通配符(Wildcard)

通配符用于灵活限制泛型的类型范围,常见有以下三种形式:

(1)<?>:无界通配符

表示支持任意泛型类型,即可以匹配所有泛型实例。
示例:

1
2
3
4
5
6
7
8
9
public void printList(List<?> list) {
for (Object obj : list) {
System.out.println(obj);
}
}

// 调用时可传入任意泛型类型的List
printList(new ArrayList<String>()); // 正确
printList(new ArrayList<Integer>()); // 正确

(2)<? extends A>:上界通配符

表示支持 A类及其所有子类 的泛型类型,限定了泛型的上限。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {}
class B extends A {}
class C extends B {}

// 只能接收 List<A>、List<B>、List<C> 等
public void printA(List<? extends A> list) {
for (A a : list) { // 可安全转型为A
System.out.println(a);
}
}

printA(new ArrayList<A>()); // 正确
printA(new ArrayList<B>()); // 正确(B是A的子类)
printA(new ArrayList<String>()); // 错误(String与A无关)

注意:使用上界通配符的集合只能读取元素(可转型为A),不能添加元素(无法确定具体子类类型,可能导致类型混乱)。

(3)<? super A>:下界通配符

表示支持 A类及其所有父类 的泛型类型,限定了泛型的下限。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class A {}
class B extends A {}
class Object {} // 假设Object是A的间接父类(实际Java中Object是所有类的父类)

// 只能接收 List<A>、List<Object> 等
public void addA(List<? super A> list) {
list.add(new A()); // 可添加A的实例
list.add(new B()); // 可添加A的子类实例(多态)
}

addA(new ArrayList<A>()); // 正确
addA(new ArrayList<Object>()); // 正确(Object是A的父类)
addA(new ArrayList<B>()); // 错误(B是A的子类,不符合下界要求)

注意:使用下界通配符的集合可以添加A及其子类的元素,但读取元素时只能转型为Object(无法确定具体父类类型)。

总结

  • 泛型无继承性,需通过通配符实现灵活的类型匹配。
  • <?>:任意类型;<? extends A>:A及其子类(上限);<? super A>:A及其父类(下限)。
  • 通配符的使用需结合场景:读取为主用上限,添加为主用下限。
  • 作业:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    package generic.coustemGeneric;

    import java.util.*;

    public class HomeWork_1 {
    public static void main(String[] args) {

    }
    }

    class User {
    private int id;
    private int age;
    private String name;

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

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public int getAge() {
    return age;
    }

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

    public String getName() {
    return name;
    }

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

    class Dao<T> {
    //成员变量
    private Map<String, T> map;

    // 保存对象
    public void save(String id, T entity) {
    map.put(id, entity);
    }

    //从map中获取id对应的对象
    public T get(String id) {
    return map.get(id);
    }

    //替换 map 中key为id的内容,改为 entity 对象
    public void update(String id, T entity) {
    map.put(id, entity);
    }

    //返回 map 中存放的所有 T 对象,要list的形式
    public List<T> list() {
    return new ArrayList<>(map.values());
    }
    }

集合

集合框架体系图

Collection接口和常用方法

●Collection接口实现类的特点

public interface Collection extends Iterable

  1. Collection实现子类可以存放多个元素,每个元素可以是Object
  2. 有些Collection的实现类,可以存放重复的元素,有些不可以
  3. 有些Collection的实现类,有些是有序的(List),有些不是有序(Set)
  4. Collection接口没有直接的实现子类,是通过它的子接口Set 和 List 来实现的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    import java.util.ArrayList;

    @SuppressWarnings({"all"})//抑制警告
    public class CollectionMethod {
    public static void main(String[] args) {
    ArrayList arrayList = new ArrayList();
    //add添加单个元素
    arrayList.add("jack");//自动装箱,object类型
    System.out.println(arrayList);
    //remove删除指定元素
    //arrayList.remove("jack");
    //contains查找元素是否存在
    if (arrayList.contains("jack")) {
    System.out.println("元素存在");
    } else {
    System.out.println("元素不存在");
    }
    //size获取元素的个数
    System.out.println(arrayList.size());
    //isEmpty判断元素是否为空
    System.out.println(arrayList.isEmpty());
    //clear清空list里面的所有元素
    arrayList.clear();
    System.out.println(arrayList);
    //addAll添加多个元素,可以放入另一个创建好了的集合
    ArrayList arrayList1 = new ArrayList();
    arrayList1.add("1");
    arrayList1.add("2");
    arrayList.addAll(arrayList1);
    //删除多个元素
    arrayList.removeAll(arrayList1);
    }
    }

Collection接口和常用方法

●Collection接口遍历元素方式1-使用Iterator(迭代器)

基本介绍
1
2
3
4
5
6
java.util
接口 Iterator<E>
所有已知子接口:
ListIterator<E>, XMLStreamReader
所有已知实现类:
BeanContextSupport.BCSIterator, EventReaderDelegate, Scanner
  1. Iterator对象称为迭代器,主要用于遍历 Collection 集合中的元素。
  2. 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
  3. Iterator 的结构
  4. Iterator 仅用于遍历集合,Iterator 本身并不存放对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package collection.homeWork;

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

@SuppressWarnings("all")
public class HomeWork_01 {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("小花", 2));
list.add(new Dog("小黑", 3));
//用增强for循环遍历
for (Object dog : list) {
System.out.println(dog);
}
//用迭代器Iterator
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object dog = iterator.next();
System.out.println(dog);
}


}
}

class Dog {
private String name;
private int age;

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

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

List

List接口和常用方法

List接口基本介绍

List 接口是 Collection 接口的子接口

  1. List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
  2. List集合中的每个元素都有其对应的顺序索引,即支持索引
  3. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号获取容器中的元素
  4. JDK API中List接口的实现类有:

常用的有:ArrayList、LinkedList和Vector

示例代码
1
2
3
4
5
6
7
8
9
List list = new ArrayList();
list.add("tom");
list.add("jack");
list.add("mary");
list.add("mary");
list.add("smith2");
list.add("kristina");
System.out.println(list);
System.out.println(list.get(4)); //smith2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package collection.homeWork;

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

@SuppressWarnings("all")
public class HomeWork_02 {
public static void main(String[] args) {
/*添加10个以上的元素(比如String"hello”),
在2号位插入一个元素"韩顺平教育"获得第5个元素,
删除第6个元素,修改第7个元素,
在使用迭代器遍历集合要求:
使用List的实现类ArrayList完成。*/
List list = new ArrayList();
//添加十个以上的元素
for (int i = 0; i < 12; i++) {
list.add("元素" + i);
}
System.out.println(list);
//在二号位插入一个元素
list.add(1, "插入位");
//获得第五个元素
System.out.println(list.get(4));
//删除第六个元素
list.remove(5);
System.out.println(list);
//修改第七个元素
list.set(6, "修改后的元素");
//使用迭代器遍历元素
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object lis = iterator.next();
System.out.println(lis);
}
}
}

常用方法

List的三种遍历方式ArrayList,linkedList,Vector这些都适用这些遍历

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

@SuppressWarnings("all")
public class List_for {
public static void main(String[] args) {
List list = new ArrayList();
//add增加一些数据
list.add("张三");
list.add("张三丰");
list.add("鱼香茄子");
list.add("小李广");
//迭代器遍历
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object lis = iterator.next();
System.out.println(lis);

}
System.out.println("*******");
//增强for循环
for (Object list_for : list) {
System.out.println(list_for);

}
System.out.println("*********");
//普通循环for
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}

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

@SuppressWarnings("all")
public class HomeWork_List {
public static void main(String[] args) {
//按照价格排序,从低到高(使用冒泡排序)
// List list = new ArrayList();
// List list = new Vector();
List list = new LinkedList();
list.add(new Book("红楼梦", "曹雪芹", 19.9));
list.add(new Book("西游记", "吴承恩", 9.9));
list.add(new Book("水浒传", "施耐庵", 9.9));
//遍历
for (Object book : list) {
System.out.println(list);
}


}
}

@SuppressWarnings("all")
class Book {
private String name;
private String author;
private double price;

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

/**
* 对图书列表按照价格进行冒泡排序(升序)
*
* @param list 图书对象列表,列表中的元素必须是Book类型
*/
public static void sort(List list) {
// 外层循环控制排序轮数
for (int i = 0; i < list.size() - 1; i++) {
// 内层循环进行相邻元素比较和交换
for (int j = 0; j < list.size() - 1 - i; j++) {
//取出对象Book
Book book = (Book) list.get(j);
Book book1 = (Book) list.get(j + 1);
if (book.price > book1.price) {
list.set(j, book1);
list.set(j + 1, book);
}
}
}
}


@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}

vector

Vector和ArrayList一样,都是Vector的子类,Vector是线程安全的,ArrayList是线程不安全的。

LinkedList和ArryList在实际开发中的选择

在实际开发中,如果改查的情况比较多的情况下选择ArrayList(线程不安全)
如果增删的情况比较多,选择LinkedList(线程不安全)

Set

Set是无序的,没有索引,不允许重复

Set接口和常用方法

和list接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样

Set接口的遍历方式

  1. 可以使用迭代器
  2. 增强for
    不能使用索引的方式来获取

数组链表模拟

一个节点中存放了两个信息,第一个是存放的是元素的值item,第二个存放的是指向下一个节点的地址。
我们用一个node类来模拟一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package collection.Set;

@SuppressWarnings("all")
public class HashSetStructure {
public static void main(String[] args) {
//创建一个Node数组
Node[] node = new Node[16];
//创建一个独立的节点出来
Node jack = new Node("jack", null);
//将节点放入数组中
node[2] = jack;
//再创建一个独立节点
Node tom = new Node("tom", null);
jack.next = tom;
//再来一个独立节点
Node lili = new Node("lili", null);
tom.next = lili;
lili.next = new Node("mike", null);

}
}

class Node {
Object item;
Node next;

public Node(Object item, Node next) {
this.item = item;
this.next = next;
}
}

List、Set 和 Map 什么时候用?它们各自适合什么场景?

我们来结合你提供的这张图,详细解释一下这三者的区别和使用场景。


🔹 一、先看结构图的含义

你的图展示了 Java 集合框架的继承关系:

1
2
3
4
5
6
7
8
9
10
Iterable

Collection
├─ List
│ ├─ ArrayList
│ ├─ Vector
│ └─ LinkedList
└─ Set
├─ TreeSet
└─ HashSet
  • Iterable:所有可遍历的集合都实现这个接口。
  • Collection:是所有单值集合的父接口,包括 List 和 Set。
  • List:允许重复元素,有索引(有序)。
  • Set:不允许重复元素(无序或有序,取决于实现)。
  • Map:不在图中,但它是另一个分支,存储“键值对”。

⚠️ 注意:Map 不继承自 Collection,它是一个独立的接口。


✅ 二、List、Set、Map 的核心区别
类型 是否允许重复 是否有序 存储方式 主要用途
List ✅ 允许 ✅ 有序(按插入顺序) 索引访问 按顺序保存数据,支持重复项
Set ❌ 不允许 ❌ 无序(HashSet) / ✅ 有序(TreeSet) 唯一性保证 去重、唯一集合
Map ✅ 键唯一,值可重复 ❌ 键有序/无序(取决于实现) 键值对 快速查找、关联数据

✅ 三、具体使用场景详解
🟦 1. 使用 List 的情况

当你需要:

  • 保留元素的插入顺序;
  • 允许重复元素;
  • 通过索引快速访问元素(如 list.get(0));

✅ 举个例子:

1
2
3
4
5
List<String> names = new ArrayList<>();
names.add("张三");
names.add("李四");
names.add("张三"); // 允许重复
System.out.println(names.get(0)); // 输出 "张三"

📌 常见使用场景:

  • 存储用户列表、订单列表、日志记录等;
  • 需要频繁增删改查的有序数据;
  • 需要随机访问元素时(ArrayList 比较快);

🔹 推荐选择:

  • ArrayList:查询快,适合读多写少;
  • LinkedList:插入删除快,适合频繁增删;

🟨 2. 使用 Set 的情况

当你需要:

  • 去重
  • 不关心顺序(或要求排序);
  • 快速判断某个元素是否存在;

✅ 举个例子:

1
2
3
4
5
Set<String> uniqueNames = new HashSet<>();
uniqueNames.add("张三");
uniqueNames.add("李四");
uniqueNames.add("张三"); // 重复会被自动忽略
System.out.println(uniqueNames.size()); // 输出 2

📌 常见使用场景:

  • 统计不重复的用户 ID;
  • 过滤重复数据;
  • 实现“黑名单”、“白名单”;
  • 需要快速判断某元素是否存在的场景;

🔹 推荐选择:

  • HashSet:无序,性能最好(O(1) 查找);
  • TreeSet:有序(自然排序),但性能稍慢(O(log n));

🟩 3. 使用 Map 的情况

当你需要:

  • 存储“键 → 值”关系;
  • 根据键快速查找对应的值;
  • 键必须唯一,值可以重复;

✅ 举个例子:

1
2
3
4
5
Map<String, Integer> scores = new HashMap<>();
scores.put("张三", 95);
scores.put("李四", 88);
scores.put("张三", 90); // 键重复,值被覆盖
System.out.println(scores.get("张三")); // 输出 90

📌 常见使用场景:

  • 用户信息管理(ID → User 对象);
  • 缓存系统(key → value);
  • 字典、配置文件解析;
  • 统计频次(如单词出现次数);

🔹 推荐选择:

  • HashMap:最常用,无序,速度快;
  • TreeMap:按键排序;
  • LinkedHashMap:保持插入顺序;

✅ 四、总结对比表
需求 推荐使用
保存有序数据,允许重复 ✅ List(ArrayList / LinkedList)
去重,不要重复元素 ✅ Set(HashSet / TreeSet)
快速根据键查找值 ✅ Map(HashMap / TreeMap)
要求元素有序且不重复 ✅ TreeSet
要求快速插入删除 ✅ LinkedList
要求快速查找 ✅ HashSet / HashMap

✅ 五、一句话口诀帮助记忆

List 有序可重复,Set 唯一去重好,Map 键值配对找。


Map

map接口特点

1
2
3
4
// 1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null)
// 2. k-v 为了方便程序员的遍历,还会创建 EntrySet 集合,该集合存放的元素的类型 Entry,而一个Entry
// 对象就有k,v EntrySet<Entry<K,V>>即: transient Set<Map.Entry<K,V>> entrySet;
//为了方便遍历才将NOde的放到entry里面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.HashMap;
@SuppressWarnings("all")
public class map_1 {
public static void main(String[] args) {
//Map与collection是并列存在的,map是映射的关系型数据
//Map中的key-value可以是任意类型
//key不能重复,如果key重复会覆盖第一个重复key的value
//key的值key为null,但是只能有一个,然后value可以有多个null
HashMap hashMap = new HashMap();
hashMap.put("No1", "test1");//任意类型
System.out.println(hashMap);
hashMap.put("No2", "test2");
hashMap.put("No3", "test3");
hashMap.put("No1", "test4_覆盖");
System.out.println(hashMap);
hashMap.put(null, null);
hashMap.put(null, "覆盖");
hashMap.put("No4", null);
System.out.println(hashMap);
}
}

Map接口和常用方法

  1. put: 添加
  2. remove: 根据键删除映射关系
  3. get: 根据键获取值
  4. size: 获取元素个数
  5. isEmpty: 判断个数是否为0
  6. clear: 清除
  7. containsKey: 查找键是否存在
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    package Map.HashMap;


    import java.util.HashMap;
    import java.util.Map;

    @SuppressWarnings("all")
    public class MapMethod {
    public static void main(String[] args) {
    Map map = new HashMap();
    map.put("1", new Book("Java", 100));
    map.put("2", "test2");
    map.put("3", "test3");
    map.put("4", "test4");
    System.out.println("map=" + map);
    //remove根据key删除vlue映射
    map.remove("3");
    System.out.println("map=" + map);
    //get根据键获取值
    System.out.println("map.get(\"2\")=" + map.get("2"));
    //size获取大小
    System.out.println(map.size());
    //clean清除
    //map.clear();
    //检查是否为空
    System.out.println(map.isEmpty());
    //containskey 根据键找是否有value返回boolean
    System.out.println(map.containsKey("1"));
    //containsvalue 根据value找是否有value返回boolean
    System.out.println(map.containsValue("test2"));
    }
    }

    @SuppressWarnings("all")
    class Book {
    private String name;
    private int num;

    public Book(String name, int num) {
    this.name = name;
    this.num = num;
    }

    @Override
    public String toString() {
    return "Book{" +
    "name='" + name + '\'' +
    ", num=" + num +
    '}';
    }
    }
  • map的六大遍历方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    // Map接口和常用方法
    // Map接口遍历方法
    // Map遍历方式案例演示 MapFor.java

    // 遍历方式:
    // 1. keySet: 获取所有的键
    // 2. entrySet: 获取所有关系 k-v
    // 3. values: 获取所有的值
    // 4. containsKey: 查找键是否存在
    import java.util.*;
    @SuppressWarnings("all")
    public class MapFor {
    public static void main(String[] args) {
    Map map = new HashMap();
    map.put("1", new Book("Java", 100));
    map.put("2", "test2");
    map.put("3", "test3");
    map.put("4", "test4");
    //第一种先取出key再遍历value(entryset里面的keyset)
    System.out.println("******************第一种方式***********************");
    Set set = map.keySet();
    //增强for
    for (Object key : set) {
    System.out.println(map.get(key));
    }
    //迭代器
    Iterator iterator = set.iterator();
    while (iterator.hasNext()) {
    Object key = iterator.next();
    System.out.println(map.get(key));
    }
    //第二种,直接出去key进行遍历(entryset里面的Collection)
    System.out.println("******************第二种方式***********************");
    Collection values = map.values();
    //增强 for
    for (Object value : values) {
    System.out.println(value);
    }
    //迭代器
    Iterator iterator1 = values.iterator();
    while (iterator1.hasNext()) {
    Object vale = iterator1.next();
    System.out.println(vale);
    }
    //第三种方式通过entryset来取出来
    System.out.println("******************第三种方式***********************");
    Set entrySet = map.entrySet();
    //增强for
    for (Object set1 : entrySet) {
    Map.Entry m = (Map.Entry) set1;
    System.out.println(m.getValue());
    }
    //迭代器
    Iterator iterator2 = entrySet.iterator();
    while (iterator2.hasNext()) {
    Object o = iterator2.next();
    Map.Entry m1 = (Map.Entry) o;
    System.out.println(((Map.Entry<?, ?>) o).getValue());
    }

    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    // Map接口课堂练习 MapExercise.java
    // 使用HashMap添加3个员工对象,要求
    // 键: 员工id
    // 值: 员工对象
    // 并遍历显示工资>18000的员工(遍历方式最少两种)
    // 员工类: 姓名、工资、员工id
    import java.util.*;

    @SuppressWarnings("all")
    public class HomeWork_01 {
    public static void main(String[] args) {
    Map map = new HashMap();
    Object jack = map.put(1, new Employee(1, "jack", 10000));
    Object tom = map.put(2, new Employee(2, "tom", 120000));
    Object tony = map.put(3, new Employee(3, "tony", 130000));
    //遍历显示工资大于18000的员工,我直接取值出来,这里取出来的值为Employee但是我们的value是objec类型需要向下转型一下
    Collection values = map.values();
    for (Object value : values) {
    if (((Employee) value).getSalary() > 18000) {
    System.out.println(((Employee) value).getName());
    }
    }
    //迭代器
    Iterator iterator = values.iterator();
    while (iterator.hasNext()) {
    Object value = iterator.next();
    if (((Employee) value).getSalary() > 18000) {
    System.out.println(((Employee) value).getName());
    }
    }
    //取key出来
    Set keys = map.keySet();
    //增强for
    for (Object key : keys) {
    if (((Employee) map.get(key)).getSalary() > 18000) {
    System.out.println(((Employee) map.get(key)).getName());
    }
    }
    //迭代器
    Iterator iterator1 = keys.iterator();
    while (iterator1.hasNext()) {
    Object keyss = iterator1.next();
    if (((Employee) (map.get(keyss))).getSalary() > 18000) {
    System.out.println(((Employee) (map.get(keyss))).getName());
    }
    }


    }
    }

    @SuppressWarnings("all")
    class Employee {
    private int id;
    private String name;
    private int salary;

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

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

    public Employee() {

    }

    public int getId() {
    return id;
    }

    public void setId(int id) {
    this.id = id;
    }

    public String getName() {
    return name;
    }

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

    public int getSalary() {
    return salary;
    }

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

    @Override
    public String toString() {
    return "Employee{" +
    "id=" + id +
    ", name='" + name + '\'' +
    ", salary=" + salary +
    '}';
    }
    }
  • Map接口的常用实现类: HashMap、Hashtable和Properties
  • HashMap是Map接口使用频率最高的实现类
  • HashMap是以key-val对的方式来存储数据(HashMap$Node类型)
  • key不能重复,但是值可以重复,允许使用null键和null值
  • 如果添加相同的key,则会覆盖原来的key-val,等同于修改(key不会替换,val会替换)
  • 与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的
  • HashMap没有实现同步,因此是线程不安全的

Map接口实现类-Hashtable

  • 存放元素类型:存放的元素是键值对,即 K-V
  • 空值限制:hashtable的键和值都不能为null,否则会抛出NullPointerException
  • 使用方法:hashTable使用方法基本上和HashMap一样
  • 线程安全性:hashTable是线程安全的(synchronized),hashMap是线程不安全的
  • 底层结构:简单看下底层结构
  • 应用:HashTable的应用案例

Map接口实现类 - Properties

  • 继承关系:Properties类继承自Hashtable类并且实现了Map接口
  • 存储形式:使用键值对的形式来保存数据
  • 使用特点:使用特点和Hashtable类似
  • 配置文件:还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改

总结-开发中如何选择集合实现类(记住)

在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

选择流程

第一步:判断存储类型

  • 一组对象 → 选择 Collection 接口的实现类
  • 一组键值对 → 选择 Map 接口的实现类

第二步:Collection接口的选择(存储一组对象)

情况1:允许重复 → 选择 List
  • 增删多LinkedList [底层维护了一个双向链表]
  • 改查多ArrayList [底层维护 Object类型的可变数组]
情况2:不允许重复 → 选择 Set
  • 无序HashSet [底层是HashMap,维护了一个哈希表(数组+链表+红黑树)]
  • 排序TreeSet
  • 插入和取出顺序一致LinkedHashSet [维护数组+双向链表]

第三步:Map接口的选择(存储一组键值对)

  • 键无序HashMap [底层:哈希表]
    • jdk7: 数组+链表
    • jdk8: 数组+链表+红黑树
  • 键排序TreeMap
  • 键插入和取出顺序一致LinkedHashMap
  • 读取文件Properties

快速选择指南

常用场景推荐

需求场景 推荐实现类 理由
随机访问多 ArrayList 数组结构,随机访问效率高
频繁增删 LinkedList 链表结构,增删效率高
去重存储 HashSet 哈希表,去重效率高
排序需求 TreeSet/TreeMap 红黑树,自动排序
保持插入顺序 LinkedHashSet/LinkedHashMap 链表维护顺序
键值对存储 HashMap 最常用的Map实现
配置文件 Properties 专门处理属性文件
线程安全集合 ConcurrentHashMap/Collections.synchronizedXXX 线程安全需求

性能对比

List接口实现类

  • ArrayList

    • 优点:随机访问快(O(1)),内存连续
    • 缺点:增删中间元素慢(O(n))
    • 适用:查询多,增删少的场景
  • LinkedList

    • 优点:增删快(O(1)),特别适合头尾操作
    • 缺点:随机访问慢(O(n))
    • 适用:频繁增删,特别是队列/栈操作

Set接口实现类

  • HashSet

    • 优点:添加、删除、查找最快(O(1))
    • 缺点:无序
    • 适用:需要快速去重的场景
  • TreeSet

    • 优点:自动排序,有序输出
    • 缺点:性能稍差(O(log n))
    • 适用:需要排序或范围查询的场景
  • LinkedHashSet

    • 优点:保持插入顺序,性能接近HashSet
    • 缺点:需要额外内存维护链表
    • 适用:需要保持顺序的去重集合

Map接口实现类

  • HashMap

    • 优点:最快的Map实现(O(1))
    • 缺点:无序
    • 适用:大多数键值对存储场景
  • TreeMap

    • 优点:键自动排序
    • 缺点:性能稍差(O(log n))
    • 适用:需要按键排序的场景
  • LinkedHashMap

    • 优点:保持插入顺序或访问顺序
    • 适用:需要保持顺序的Map

Collections工具类

Collections工具类介绍

  • 工具类定位:Collections是一个操作Set、List和Map等集合的工具类
  • 功能特点:Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

@SuppressWarnings("all")
public class collection_util {
public static void main(String[] args) {
//创建一个ArrayList用于测试
ArrayList arrayList = new ArrayList();
arrayList.add("tom");
arrayList.add("smith");
arrayList.add("king");
arrayList.add("peter");
System.out.println(arrayList);
//reverse(List):反转List中元素的顺序
Collections.reverse(arrayList);
System.out.println("arryList=" + arrayList);
//shuffle(List):对List集合元素进行随机排序
Collections.shuffle(arrayList);
System.out.println("shuffle=" + arrayList);
//sort(List):根据元素的自然顺序对指定List集合元素按升序排序
Collections.sort(arrayList);
System.out.println("sort=" + arrayList);
//srot(List.Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
//例如:我们希望按照字符串长度大小排序
Collections.sort(arrayList, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("按照字符串长度大小排序=" + arrayList);
//swap(list,int,int):将指定list集合中i处元素和j处元素进行交换
Collections.swap(arrayList, 0, 3);
System.out.println("swap=" + arrayList);
//Object max(Collection):根据元素的自然顺序,返回集合中的最大元素
System.out.println("max=" + Collections.max(arrayList));
//Object max(Collection,Comparator):根据Comparator 指定的顺序,返回给定集合中的最大元素
Object max = Collections.max(arrayList, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
});
System.out.println("max+comparator=" + max);
//min 按照自然排序的最小
System.out.println("min=" + Collections.min(arrayList));
//min comparator按照指定的排序最小
System.out.println("min comparator=" + Collections.min(arrayList, new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String) o1).length() - ((String) o2).length();
}
}));
//int frequency(Collection,objcet):返回指定集合中指定元素出现的次数
System.out.println(Collections.frequency(arrayList, "tom"));
//void copy(list dest,list src): 将src中的内容复制到dest中
ArrayList arrayList1 = new ArrayList();
arrayList1.add("1");
arrayList1.add("2");
arrayList1.add("3");
Collections.copy(arrayList, arrayList1);
System.out.println(arrayList);//[1, 2, 3, tom] arryList1长度不够所以没有替换完
//boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
Collections.replaceAll(arrayList, "tom", "tomcat");
System.out.println(arrayList);
}
}