重温java基础知识

重温java基础知识
Silence前言
自从准备考试以后很久都没有整过我自己的老本行了,很多东西都忘得差不多了,我决定在这个学期把丢下的这些东西全都捡起来
还有硬件的一些专业知识,包括最新的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 | package diyizhang; |
精度保留
1 | // 示例数据:计算 10/3 的结果 |
局部变量的类型转换
重要部分:只有初始化变量时var才能用来声明变量,初始化变量就是赋一个值
1 | var b;//这种写法是错的 |
数组,字符串,正则表达式
一维数组的定义
定义需要经过三个步骤:声明数组,数组分配内存空间,创建数组空间并且赋值
声明
数据类型[] 数组名
这里与c不同的是c是将 数组名和[] 调换了方向的。本人一开始学习的c,所以更倾向于c的写法
数组分配内存空间
分配内存给数组
1 | 数组名 = new 数据类型[个数] |
数组创建后就不能修改其大小
一维数组的初始化
在定义数组的同时就为数组元素分配空间并赋值。
1 | 数据类型 [] 数组名 = {初值1,初值2,初值3,.....} |
二维数组
二维数组初始化
1 | // 方式1 分开来 |
上述的错误示范其实很好理解
这是我问ai的出来的最通俗的理解
就是如果这个是一块地皮需要建楼
行代表我要建几层楼,至于每层楼建几米没有确定这个是能理解的
但是如果先说每层楼建几米,但是没说整栋楼要建基层是不对的这样就延申下来了以下的问题
杨辉三角形的问题里面
1 | int i, j; //定义行列 |
解释:
用搭积木的思维来理解
想象一下你要用积木搭建一个金字塔:
1 | java |
这个过程就像:
你先申请了一块地,说这里要盖7层楼 (new int[7][])
然后你从第一层开始,每层搭建不同长度的墙体
第一层:搭建1米长的墙 (new int[1])
第二层:搭建2米长的墙 (new int[2])
…
第七层:搭建7米长的墙 (new int[7])
字符串
String型字符串
字符常量 使用单引号 ‘’ 括起来,并且里面必须且只能有一个字符。
1 | char letter = 'A'; // 正确:一个大写字母A |
1 | String greeting = "Hello, World!"; // 正确:多个字符组成的序列 |
类与对象
参数的传递
1 | package diyizhang; |
在这里有个问题是如果我少传一个参数该怎么办
1 | volu.setCylinder(2.5, 5, 3.14); |
很显然,这个是不能的。但是我们可以使用方法重载
1 | void setCylinder(double radius, int h, double p) { |
this的使用
1 | class Cylinder { |
这里的radius成员变量和局部变量是相同的变量,如果我成员变量用作算其他的式子,局部变量算另一个式子。这样就会出现混乱
当特指成员变量时,要用this关键字
以数组作为参数进行传递
1 | public class App6_5{ |
从例子中看出,如果想要将参数传递到方法中,只需要在方法名后面传入数组名就可以了。
如果传入的时
1 | minNumber.least(a[1]); |
返回值为数组类型的方法
若方法需要返回一个数组,则必须在该方法名之前加上数组类型的修饰符,例如返回一维数组
int [] ,返回二维数组 int [][]
1 | int [] test(int [] a){ |
可变参数
参数的接收可以是固定的,或者不固定的。方法接收不固定的情况则为可变参数
方法接收可变参数的方法为
1 | 返回值类型 方法名 (固定参数列表,数据类型 ...可变参数名){ |
当有多个参数的情况下,可变参数只能位于最后一个
调用可变参数的时候,编译器为可变参数创建一个隐含的数组,通过调用数组的形式来访问可变参数
匿名对象
当一个对象被创建时可以不用那么麻烦的创建一个对象的引用变量,直接调用对象的方法
1 | //这个是原始的写法 |
1 | //这个是匿名对象的写法 |
使用场景:
- 这个对象只需要一次调用的话就可以这样使用
- 将匿名对象作为实参传递给一个方法调用。
java语言类的特性
类的私有成员与公共成员
无访问控制符
若在类成员的前面不加任何访问控制符,则该成员具有默认访问控制符
表示这个成员变量只能在相同的包内进行访问和调用
方法重载
方法的含义相同,但带有不同的参数,这些方法使用相同的名字,这就叫方法的重载
- 注意:
仅仅参数的变量名不同是不行的
参数个数不同,参数的类型不同,参数的顺序不同
1 | class Cylinder{ |
构造方法详解
我理解你对构造方法这个概念感到困惑。让我通过你的代码示例来详细解释构造方法的作用和特点。
构造方法的基本概念
构造方法是一种特殊的方法,它在创建对象时自动调用,主要用于初始化对象的属性。
代码分析
1 | class Cylinder { |
构造方法重载
在类中同时存在无参构造,有参构造这种就算构造方法重载
从一个构造方法调用另一个构造方法
在类中有多个方法,这些方法中可以相互调用,通过使用this()语句来调用的
1 | //FileName: App7_5.java |
static 和 final 的组合作用
static 和 final 虽然经常一起使用,但它们的作用是完全不同的。
两者的区别
| 关键字 | 作用 | 解决的问题 |
|---|---|---|
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 | public class Test { |
为什么数学常量需要这样设计?
1. 数学真实性
π的值是固定不变的,不应该允许被修改
1 | // 如果只有static,可能发生: |
2. 内存效率
π的值对所有对象都是一样的,不需要每个对象都存储一份
1 | // 如果没有static: |
3. 访问便利性
不需要创建对象就能使用
1 | // 有static:直接使用 |
其他常见的使用场景
颜色常量
1 | public class Colors { |
错误代码
1 | public class ErrorCodes { |
配置参数
1 | public class Config { |
总结
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 | Integer i = Integer.valueOf(10); // 手动装箱 |
JDK 5 之后(自动):
1 | Integer i = 10; // 自动装箱:编译器背后执行的仍是 Integer.valueOf(10) |
2. 对应的类型关系表
| 基本类型 | 包装类 |
|---|---|
byte |
Byte |
short |
Short |
int |
Integer |
long |
Long |
float |
Float |
double |
Double |
char |
Character |
boolean |
Boolean |
3. 发生的常见场景
a) 赋值时(最常见)
这是最直接的自动装箱和拆箱。
1 | // 赋值时的装箱和拆箱 |
b) 方法调用时(非常重要)
当传入方法的参数类型与实际值类型不匹配时,会自动发生。
1 | public static void printInteger(Integer i) { |
1 | public static int add(int a, int b) { |
c) 集合操作时(使用频率极高)
Java 的集合框架(如 ArrayList, HashMap)只能存储对象,不能存储基本类型。自动装箱让我们可以像直接存基本类型一样方便。
1 | ArrayList<Integer> list = new ArrayList<>(); |
d) 运算和比较时
在数学运算中,包装类会自动拆箱为基本类型进行计算。在三元运算符、==、>、< 等混合运算中,也会发生自动拆箱和装箱。
1 | Integer a = 10; |
4. 注意事项和陷阱(面试常考!)
自动装箱虽然方便,但也带来了一些容易忽略的问题。
陷阱一:== 比较的陷阱
== 在比较对象时,比较的是内存地址(引用是否指向同一个对象),而不是值。
情况1:值的范围在 [-128, 127] 之间
Java 对这部分常用的 Integer 对象进行了缓存,Integer.valueOf() 会返回缓存中的对象。
1 | Integer a = 127; |
情况2:任何范围,== 比较包装类和基本类型
这时包装类会自动拆箱,然后比较值。
1 | Integer a = 128; |
最佳实践:比较包装类的值,永远使用 .equals() 方法!
1 | Integer a = 128; |
陷阱二:空指针异常(NullPointerException)
包装类可以是 null,而基本类型不能。如果在拆箱一个 null 的包装类,就会抛出异常。
1 | Integer nullInteger = null; |
这种情况常发生在从方法返回 null 或从集合中取出 null 值时,需要格外小心。
陷阱三:性能开销
虽然微小,但装箱和拆箱确实会创建对象和调用方法,存在额外的性能开销。在大量的循环(如千万次、亿次)中,这种开销会累积变得显著。
- 高性能场景: 应优先使用基本类型(如
int),避免无意识的自动装箱。 - 一般业务场景: 为了方便,使用自动装箱完全没有问题。
Java 包装类与自动装箱的应用场景
概述
简单来说,使用包装类是为了弥补基本类型的局限性,让它们能在”面向对象”的世界里更好地工作。而自动装箱/拆箱是为了消除使用包装类时的繁琐代码,让我们写起来更方便。
核心应用场景
场景一:让基本类型能存入集合(最核心的原因)
这是最常用、最重要的原因。Java的集合框架(如 ArrayList, HashMap, HashSet)在设计上有一个基本原则:它们只能存储对象(Object),不能存储基本类型(int, double 等)。
为什么集合不能存基本类型?
因为集合的设计需要通用性,要能存放任何类型的数据。而”任何类型”在Java里最顶层的父类就是 Object。基本类型不是对象,不从 Object 继承,所以被排除在外。
没有包装类和自动装箱时(JDK5以前):
1 | // 1. 想存一个int到ArrayList里,必须先手动"装箱" |
太麻烦了! 每次存取的代码都变得很长。
有了包装类和自动装箱后:
1 | // 现在我们可以这样写,直观又简洁 |
结论: 为了让基本数据类型也能方便地存入集合这种强大的容器中,我们必须使用包装类,而自动装箱/拆箱让这个过程毫不费力。
场景二:表示”缺失”或”未知”的值(空值)
基本类型都有默认值(如 int 默认是 0)。但在很多业务场景下,0 是一个有效的数值,而不是”未知”或”未设置”。
例子:统计学生年龄
1 | // 使用基本类型 int - 有歧义 |
结论: 包装类可以用 null 来表示值的缺失,这在数据库查询、JSON解析等场景中极其常见(因为数据库中的字段很多就是可空的)。
场景三:使用泛型时
泛型(Generics)是JDK5另一个重大特性,它也和集合一样,类型参数必须是类类型,不能是基本类型。
你不能这样写:
1 | List<int> list = new ArrayList<>(); // 编译错误! |
你只能这样写:
1 | List<Integer> list = new ArrayList<>(); // 正确! |
所以,只要你使用泛型(无论是集合还是自己定义的泛型类/方法),涉及到基本类型时,就必须使用其包装类。
场景四:使用方法时,需要对象而非值
有些API或方法设计就是要求传入一个对象,以便进行一些内部操作(比如同步锁、或需要修改传入的参数值)。
例如:使用同步锁
1 | // 你可以用一个Integer对象作为锁(虽然不常见,但可行) |
虽然基本类型 int 不能这么用,但它的包装类 Integer 可以。
总结:如何选择用哪个?
| 特性 | 基本类型 (e.g., int) |
包装类 (e.g., Integer) |
|---|---|---|
| 本质 | 纯数据值 | 对象 |
| 存储 | 在栈内存,效率极高 | 在堆内存,有开销 |
| 默认值 | 有(如 int 是 0) |
null |
| 用途 | 性能优先的场景 | 功能优先的场景 |
日常编码指南:
- 定义类的成员属性:优先考虑包装类
- 定义方法的局部变量:优先使用基本类型
- 使用集合和泛型:必须使用包装类
JAVA语言垃圾回收机制
在整个java运行机制中,java运行环境提供了一套java回收机制。
其实核心就是计算调用的次数,每个对象都是有一个计数器的,当对象被调用一次的时候,该对象的计数器加一,反之则减一,当到最后的时候,该对象计数器
为0则可以判定为该对象没怎么被使用,则被回收避免内存的浪费。
1 | String str1 = "This is a String" |
当执行到第三行的时候对象仍然被str2使用,所以此时不能被回收,最后一句没有调用str1/str2使用了则可以回收了
类的继承
一个父类有可以有多个子类,一个子类只能有一个直接父类
在java语言中所有类都是直接或间接继承objec类得到的
1 | package diyizhang; |
由此可见,本来我们创建的是Student的对象,构造方法来说,应该调用的是Student()构造方法,但是这里调用了父类的构造方法
java语言继承中,执行子类的构造方法之前,会先调用父类中没有参数的构造方法,其目的是要帮助继承自父类的成员做初始化操作
调用父类中特定的构造方法(super)
这里我们知道,在创建Student对象时,默认调用父类的无参构造,那么现在我需要调用有参构造呢?
super关键字是指向该super类的父类
调用父类有参构造,需要在子类的构造函数中进行,然后用super搞定
1 | package diyizhang; |
子类中访问父类的成员
在子类中不仅能访问父类的构造方法,还能访问父类非private的成员变量和成员方法,
但super不能访问在子类中添加的成员
super.变量名
super.方法名()
覆盖
与前面的重载不同的是,重载是指在一个类里面定义多个名称相同但参数个数或类型不同的方法
覆盖则是指在子类中定义名称,参数个数与类型均与父类中完全相同的方法
- java中提供了一个注解@Override,该注解只用于方法,用来限定必须覆盖父类中的方法
注意 :子类中不能覆盖父类中声明为final和static的方法
1 | // 父类:动物 |
用父类的对象访问子类的成员
如果希望通过父类类型的引用访问子类成员,需要满足两个条件:
该成员在父类中已定义(即子类覆盖了父类的方法,或继承了父类的属性)
通过多态(父类引用指向子类对象)的方式访问
1 | // 父类 |
final成员与final类
如果一个类或成员被声明为final,则该成员不会被覆盖,则为最终变量
Object
暂存
抽象类
1. 什么是抽象类?
抽象类是用 abstract 关键字修饰的类,它不能被实例化(即不能创建对象)。抽象类通常作为其他类的基类(父类),用于定义公共接口和部分实现。
1 | public abstract class Animal { |
// 使用
Employee emp = new FullTimeEmployee(“张三”, 1001, 8000);
System.out.println(emp.getDetails());
System.out.println(“月薪: “ + emp.calculateSalary());
7. 总结
抽象类用 abstract 关键字修饰
不能被实例化,只能被继承
可以包含抽象方法和具体方法
子类必须实现所有抽象方法,否则也必须声明为抽象类
适合用于有共同特征和行为的类的模板设计
抽象类是Java面向对象编程中实现代码重用和多态性的重要工具,常用于框架设计和大型项目的架构中。
接口
- 接口是什么?
定义:接口是一个完全抽象的类型。它是一组行为规范的集合,只声明“应该做什么”,而不关心“如何做”。
核心思想:定义标准,实现解耦。它主要体现的是一种 “像…一样” (can-do) 的关系,而不是“是” (is-a) 的关系。
关键字:interface
- 接口的特点 (Java 7及以前)
在 Java 8 之前,接口是一个非常纯粹的“契约”。
成员变量 (字段):
默认都是 public static final 的常量。
必须显式初始化。
通常用于定义一些全局常量。
1 | interface USB { |
方法:
所有普通方法默认都是 public abstract 的抽象方法(没有方法体)。
没有构造方法,不能被实例化。
1 | interface Animal { |
- 接口的实现
关键字:implements
规则:
一个类使用 implements 关键字来实现一个或多个接口。
实现类必须重写(Override) 接口中所有的抽象方法,并提供具体的实现。
如果实现类没有重写所有抽象方法,那么它自己必须被声明为 abstract 抽象类。
1 | // 定义一个接口 |
- 接口的新特性 (Java 8+)
Java 8 对接口进行了重大增强,引入了默认方法和静态方法。
4.1 默认方法 (Default Methods)
关键字:default
目的:允许在接口中添加新方法,而不会破坏已有的实现类。
特点:
有方法体,提供了默认实现。
实现类可以直接继承使用这个默认实现,也可以选择重写它。
1 | interface Vehicle { |
4.2 静态方法 (Static Methods)
关键字:static
目的:将工具方法与接口相关联,避免为工具方法创建额外的类。
特点:
有方法体。
属于接口本身,只能通过接口名直接调用,不能被实现类的对象调用。
1 | interface MathOperation { |
4.3 私有方法 (Private Methods) (Java 9+)
关键字:private
目的:作为默认方法或静态方法的辅助方法,用于抽取公共代码,减少冗余,但又不暴露给外部。
分类:
private:仅供接口内的默认方法使用。
private static:供接口内的默认方法和静态方法使用。
1 | interface Logger { |
- 接口的继承
接口可以使用 extends 关键字继承其他接口。
接口支持多重继承(一个接口可以继承多个父接口),这是与类(单继承)最大的不同之一。
1 | interface Animal { |
- 接口 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 | // 1. 定义一个「可充电」接口 |
在这个例子中我发现了一个我不知道的东西
1 | // 使用接口类型引用对象(多态的体现) |
深入理解:Chargeable[] devices = {myPhone, myLaptop};
- 提高代码的灵活性和可扩展性// 原有的代码完全不需要修改!
1
2
3
4
5
6
7// 假设我们新增一个平板类
class Tablet implements Chargeable {
public void charge() {
System.out.println("平板电脑充电中");
}
}
Tablet myTablet = new Tablet();
Chargeable[] devices = {myPhone, myLaptop, myTablet}; // 直接添加新设备 - 统一处理不同类型的对象
1
2
3
4
5
6
7
8
9// 如果没有接口,你需要这样写:
Phone[] phones = {...};
Laptop[] laptops = {...};
Tablet[] tablets = {...};
// 有了接口,只需要一个循环处理所有设备
for (Chargeable device : devices) {
device.charge(); // 统一调用充电方法
} - 降低代码耦合度// 可以传入任何实现了Chargeable接口的对象数组
1
2
3
4
5
6// 方法参数使用接口类型,可以接受任何实现类
public void chargeAllDevices(Chargeable[] devices) {
for (Chargeable device : devices) {
device.charge();
}
}
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
44interface Animal {
void makeSound();
}
class Dog implements Animal {
public void makeSound() {
System.out.println("汪汪!");
}
public void fetch() { // Dog特有的方法
System.out.println("捡球");
}
}
class Cat implements Animal {
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:集合框架(最经典的例子)场景2:策略模式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());
}重要规则总结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
34interface PaymentStrategy {
void pay(double amount);
}
class CreditCardPayment implements PaymentStrategy {
public void pay(double amount) {
System.out.println("使用信用卡支付: $" + amount);
}
}
class PayPalPayment implements PaymentStrategy {
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 | 你:打电话问"衣服好了吗?" |
缺点:效率低下,浪费资源
方式二:回调(高效)
1 | 你:留下电话号码,"洗好后通知我" |
优点:高效、解耦、异步处理
代码实现
1. 定义回调接口(协议)
1 | /** |
2. 调用方实现回调接口
1 | /** |
3. 被调用方使用回调接口
1 | /** |
4. 运行演示
1 | /** |
运行结果
1 | ========== 接口回调演示开始 ========== |
核心概念解析
1. 解耦 (Decoupling)
- 干洗店不需要知道具体的客户是谁
- 客户不需要知道干洗店的具体工作流程
- 双方只通过接口进行通信
2. 控制反转 (Inversion of Control)
- 传统:调用方控制整个流程
- 回调:被调用方在适当时机”回调”调用方
- 遵循”好莱坞原则”:不要打电话给我们,我们会打电话给你
3. 异步处理 (Asynchronous Processing)
- 调用方不必阻塞等待
- 被调用方完成工作后自动通知
- 提高资源利用率和响应性
实际应用场景
Android开发中的点击事件
1 | button.setOnClickListener(new View.OnClickListener() { |
JavaScript中的事件处理
1 | document.getElementById('myButton').addEventListener('click', function() { |
网络请求回调
1 | httpClient.get(url, new HttpResponseCallback() { |
总结
接口回调的三要素
- 回调接口:定义通信协议
- 调用方:实现接口,提供回调方法的具体逻辑
- 被调用方:持有接口引用,在适当时机调用回调方法
核心价值
- 灵活性:易于扩展和修改
- 可维护性:代码结构清晰,职责分离
- 复用性:回调接口可以被多个类实现
- 异步支持:适合处理耗时操作
记忆技巧
“留个电话,完事儿叫我”
- 留电话 = 注册回调接口
- 完事儿 = 被调用方完成工作
- 叫我 = 执行回调方法
接口回调是现代编程中非常重要的概念,掌握它对于理解事件驱动编程、异步处理和各种设计模式都有很大帮助。
接口的继承
接口也具备继承性,接口继承与类的继承不同的是,接口可以有多个父类,中间用逗号隔开,新接口将继承父接口中的所有常量,抽象方法和默认方法,但不能继承父接口中的静态方法和私有方法
也不能被实现类所继承如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有抽象方法
枚举
包
java语言中引入了包的概念来管理类名空间。就像文件夹把各种文件进行管理一样。一种区别类名空间的机制
异常处理
异常是指在程序运行中由代码产生的一种错误。
异常处理机制
- 抛出异常
程序的运行中,发生了异常事件,则产生代表该异常的一个异常对象,交给运行系统,由运行系统去找寻对应的代码来处理相关异常 - 捕获异常
异常抛出后,运行系统从生成的异常对象代码开始,沿着方法的调用栈逐层回溯查找,直到找到包含相应处理异常的方法,并把异常对象提交给该方法为止
异常处理类
异常类的最顶层有一个单独的类为:Throwable,该类派生出了Error和Exception两个子类其中Error由系统保留,我们一般使用Excption
Exception异常分类
运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
运行时异常和编译时异常其实很好区分,当你在编码阶段没有报错,而点击运行代码后抛出了错误后这个其实就是运行错误
而编译错误就是在编码阶段就会提醒你的错误,所以编码错误也称检查错误
异常的处理
异常处理是通过try,catch,finally,thow,thows五个关键字实现的
- try-catch-finally语法格式:
1
2
3
4
5
6
7
8
9
10
11
12
13try {
// 可能会抛出异常的代码
// 业务逻辑代码
} catch (ExceptionType1 e1) {
// 处理 ExceptionType1 类型的异常
System.out.println("捕获到异常: " + e1.getMessage());
} catch (ExceptionType2 e2) {
// 处理 ExceptionType2 类型的异常
System.out.println("捕获到异常: " + e2.getMessage());
} finally {
// 无论是否发生异常都会执行的代码
// 通常用于资源清理,如关闭文件、数据库连接等
} - 多异常处理机制:
多异常处理机制:一个
try块后可跟多个catch块,分别处理不同类型异常;catch块能捕获父类异常时,也能捕获其所有子类异常。异常匹配流程:
try抛异常后,程序先到第一个catch块匹配,匹配即进入该块执行;不匹配则依次往后找,直到找到能接收的catch块。若都不匹配,回退到上层方法找,最终无匹配则由Java运行系统处理(通常终止程序并输出信息)。无异常情况:
try内语句无异常时,所有catch块都不执行。
1 | try { |
Java异常中 throw 和 throws 的区别
🎯 核心概念一句话总结
- **
throw**:制造问题(主动扔出一个炸弹) - **
throws**:提前警告(挂个牌子说”这里有炸弹危险”)
🍎 生活化比喻
场景:你点了一份外卖
throw 就像厨师发现菜有问题时…
1 | public void 做菜() { |
厨师说:”这菜我做不了!问题就在这!”
throws 就像菜单上的警告…
1 | public void 做辣子鸡() throws 可能太辣异常, 可能上火异常 { |
菜单说:”这道菜可能会很辣,吃了可能上火,请您知悉!”
💻 代码示例(一步步来)
第一步:只有 throw(制造问题)
1 | public class 测试 { |
运行结果:程序崩溃,控制台显示 IllegalArgumentException: 年龄不能为负数!
第二步:加上 throws(提前警告)
1 | public class 测试 { |
运行结果:捕获到异常: 年龄不能为负数!(程序不会崩溃)
📋 最简对比表
| 特性 | throw |
throws |
|---|---|---|
| 是什么 | 动作 - 扔出异常 | 声明 - 警告可能有问题 |
| 在哪里 | 方法内部 | 方法开头(签名里) |
| 后面跟什么 | new Exception()(异常对象) |
Exception.class(异常类型) |
| 例子 | throw new 炸弹(); |
throws 可能爆炸警告; |
🎯 实际应用场景
场景1:用户注册
1 | public class 用户服务 { |
场景2:调用这个方法时
1 | public static void main(String[] args) { |
❓ 常见疑问解答
问:为什么要用 throws?直接 throw 不行吗?
答:throws 是给调用者的温馨提示:
- 没有
throws:调用者不知道可能出什么错,突然就崩溃了 - 有
throws:调用者知道可能出哪些错,可以提前准备处理方案
问:什么时候必须用 throws?
答:当你抛出检查型异常时(比如 IOException, SQLException),Java强制要求你必须声明 throws。这是Java的安全机制。
💡 总结
可以把 throw 想象成 扔手榴弹,throws 想象成 贴警告标志。
throw:负责制造危险throws:负责提前告知危险
java语言的输入输出与文件处理
文件的创建方法
1 | import java.io.File; |
获取文件信息
- 获取文件的信息
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
26import 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 | import java.io.File; |
I0流原理及流的分类
I0流原理
- I/O是Input/Output的缩写,I/0技术是非常实用的技术,用于处理数据传输。
如读/写文件,网络通讯等。 - Java程序中,对于数据的输入/输出操作以”流(stream)”的方式进行。
- java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过方法输入或输出数据
流的分类
流的分类
按操作数据单位不同分为:字节流(8 bit),字符流(按字符) 效率高一些
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流
IO流体系图-常用的类
InputStream:字节输入流
InputStream 抽象类是所有字节输入流的超类。
InputStream 常用的子类:
FileInputStream:文件输入流
BufferedInputStream:缓冲字节输入流
ObjectInputStream:对象字节输入流
FileInputStream
这里讲了两种写法,第一种是循环读取一个一个的读取字节数并且转成char类型
这里我觉得IO流这边很多异常处理的地方,所以我统一向上抛了
1 | import java.io.FileInputStream; |
这里是用数组的形式读取,其实就是8个8个的读取
1 | import java.io.FileInputStream; |
FileOutputStream
其实跟输入流大差不差的,就是将数据写入到文件中
- 同样的是先测试一个字节的写入,同时注意,这里是直接覆盖了文件原有的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import 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();//关闭
}
} - 写入多个内容的时候,这里用字符串输入,然后用字符串自带的转byte的功能转换就行了(同样也是覆盖原有的内容)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import 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();//关闭
}
} - 要想不覆盖原有的内容也很简单
1
FileOutputStream fileOutputStream = new FileOutputStream(FilePath,true);//也就是追加的方式写入
文件拷贝
完成 文件拷贝 将
思路分析
先从文件的输入流,到java中去,然后再从java中输出文件文件很大循环
完成程序时,应该是读取部份数据,就写入到指定位置
1 | import java.io.FileInputStream; |
- 注意: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 | import java.io.FileWriter; |
🎯 使用要点
1. 资源管理
1 | // 推荐:使用 try-with-resources 自动关闭 |
2. 刷新缓冲区
1 | FileWriter writer = new FileWriter("file.txt"); |
3. 模式选择
1 | // 覆盖模式 - 文件存在则清空重写 |
⚠️ 常见错误
忘记关闭或刷新流
1
2
3
4// 错误:内容可能不会写入文件
FileWriter writer = new FileWriter("file.txt");
writer.write("内容");
// 缺少 close() 或 flush()使用错误的模式
1
2// 如果不想覆盖原有内容,记得使用追加模式
FileWriter writer = new FileWriter("log.txt", true);
✅ 最佳实践
- 始终使用 try-with-resources
- 根据需求选择合适的模式(覆盖/追加)
- 及时关闭或刷新流
- 处理 IOException 异常
1 | import java.io.FileWriter; |
节点流和处理流
- 节点流可以从一个特定的数据源读写数据,如FileReader、FileWriter
- 处理流(也叫包装流)是“连接”在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,如BufferedReader、BufferedWriter
📚 基本概念
节点流 (Node Stream)
- 定义:直接从特定数据源读写数据的流
- 特点:数据源的直接连接点
- 比喻:直接的水龙头
处理流 (Processing Stream) / 包装流 (Wrapper Stream)
- 定义:连接在已有流之上,提供增强功能的流
- 特点:不直接连接数据源,而是包装其他流
- 比喻:水龙头上的过滤器或增压器
🆚 对比表格
| 特性 | 节点流 | 处理流 |
|---|---|---|
| 数据源连接 | 直接连接 | 间接连接(通过包装其他流) |
| 功能 | 基础读写操作 | 增强功能(缓冲、转换等) |
| 独立性 | 可独立使用 | 必须依赖其他流 |
| 性能 | 相对较低 | 通常更高(通过缓冲等机制) |
| 例子 | FileReader, FileWriter |
BufferedReader, BufferedWriter |
💡 核心理解
设计模式:装饰器模式
处理流基于装饰器模式设计,允许动态地为对象添加功能。
流的关系
1 | 数据源 ← 节点流 ← 处理流 ← 处理流 ← 程序 |
📝 代码示例
1. 纯节点流使用(低效方式)
1 | // 类比:直接对着水龙头喝水 |
2. 节点流 + 处理流(推荐方式)
1 | // 类比:水龙头 + 水桶,一次接很多水 |
3. 多层处理流包装
1 | // 文件 → 文件流 → 缓冲流 → 数据流 |
🛠️ 常见流分类
节点流示例
| 数据源 | 输入流 | 输出流 |
|---|---|---|
| 文件 | FileInputStreamFileReader |
FileOutputStreamFileWriter |
| 内存数组 | ByteArrayInputStreamCharArrayReader |
ByteArrayOutputStreamCharArrayWriter |
| 管道 | PipedInputStreamPipedReader |
PipedOutputStreamPipedWriter |
处理流示例
| 功能 | 输入流 | 输出流 |
|---|---|---|
| 缓冲 | BufferedInputStreamBufferedReader |
BufferedOutputStreamBufferedWriter |
| 数据类型 | DataInputStream |
DataOutputStream |
| 对象序列化 | ObjectInputStream |
ObjectOutputStream |
| 转换 | InputStreamReader |
OutputStreamWriter |
✅ 最佳实践
1. 正确的关闭方式
1 | // 推荐:使用try-with-resources |
2. 流组合原则
1 | // 良好的流组合 |
3. 资源管理
- 只需要关闭最外层的处理流
- 内层流会自动关闭
- 推荐使用try-with-resources确保资源释放
🎯 核心要点总结
- 节点流是基础:建立与数据源的直接连接
- 处理流是增强:提供缓冲、转换、数据类型处理等高级功能
- 灵活组合:可以根据需要多层包装处理流
- 性能优化:处理流通常通过缓冲机制提升I/O性能
- 资源管理:正确处理流的关闭顺序和异常情况
BufferedReader
1 | import java.io.BufferedReader; |
BufferedWrite
1 | import java.io.BufferedWriter; |
用BufferedWrite和BufferedRead进行文本文件的拷贝
1 | import java.io.*; |
用BufferedInputStream和BufferedOutputStream进行音频视频图片的拷贝
1 | import java.io.*; |
节点流和处理流 - 对象流
需求场景
- 将
int num = 100这个 int 数据保存到文件中,注意不是保存数字”100”,而是保存 int 类型的 100,并且能够从文件中直接恢复 int 100 - 将
Dog dog = new Dog("小黄", 3)这个 dog 对象保存到文件中,并且能够从文件恢复
序列化和反序列化
序列化 (Serialization)
- 在保存数据时,保存数据的值和数据类型
- 将对象转换为字节序列的过程
反序列化 (Deserialization)
- 在恢复数据时,恢复数据的值和数据类型
- 将字节序列恢复为对象的过程
对象流类
ObjectOutputStream
- 序列化功能,将对象转换为字节流写入文件
ObjectInputStream
- 反序列化功能,从文件中读取字节流并恢复为对象
实现序列化的条件
要让某个对象支持序列化机制,其类必须是可序列化的。必须实现以下接口之一:
Serializable 接口
- 这是一个标记接口,没有需要实现的方法
- 推荐使用,比较简单
Externalizable 接口
- 该接口有方法需要实现
- 一般使用上面的 Serializable 接口
示例代码结构
1 | // 需要序列化的类必须实现 Serializable |
1 | import java.io.*; |
1 |
|
节点流与处理流核心区别及序列化注意事项
本文将清晰梳理节点流与处理流的核心差异,并系统整理序列化与反序列化操作的关键注意事项。
一、节点流与处理流核心区别
节点流和处理流的本质区别在于是否直接连接数据源/目的地,以及功能定位的不同。
| 对比维度 | 节点流(Node Stream) | 处理流(Processing Stream) |
|---|---|---|
| 数据源连接 | 直接连接数据源或目的地 | 不直接连接,而是包裹节点流或其他处理流 |
| 功能定位 | 负责基础的读写数据操作,是IO操作的基础 | 对已有流的数据进行加工处理,如缓冲、转换、过滤等 |
| 依赖关系 | 可独立使用,无需依赖其他流 | 必须依赖节点流或其他处理流才能工作 |
| 典型示例 | FileInputStream、FileOutputStream、FileReader | BufferedInputStream、BufferedReader、ObjectOutputStream |
二、序列化与反序列化注意事项
在使用处理流(如ObjectOutputStream/ObjectInputStream)进行对象序列化或反序列化时,需严格遵循以下规则。
读写顺序必须一致
序列化时写入对象的顺序,与反序列化时读取对象的顺序必须完全相同,否则会抛出EOFException或ClassCastException。类必须实现Serializable接口
只有实现了java.io.Serializable接口的类,其对象才能被序列化。该接口为标记接口,不含任何抽象方法,仅用于标识类具备序列化能力。建议显式添加SerialVersionUID
显式声明private static final long serialVersionUID,可固定类的版本标识。若不添加,JVM会根据类结构自动生成,类结构(如属性、方法)修改后会生成新值,导致旧版本序列化文件无法反序列化,影响版本兼容性。static与transient修饰的成员不序列化
序列化时,默认对对象所有非静态、非瞬态属性进行序列化;static修饰的静态属性属于类,transient修饰的瞬态属性为临时数据,两者均不会被序列化。属性类型需同样实现Serializable
若对象的某个属性为自定义类型,该自定义类型也必须实现Serializable接口,否则序列化时会抛出NotSerializableException。序列化具备可继承性
若父类已实现Serializable接口,其所有子类会默认继承该能力,无需再显式实现Serializable接口,子类对象可直接参与序列化。
标准输入流输出流
1 | import java.io.BufferedInputStream; |
乱码引出转换流
1 | //读取文件 |
1 | 保存文件 |
PrintStream
1 | import java.io.IOException; |
PrintWriter
1 | import java.io.FileWriter; |
泛型与集合
泛型
这种情况可以是可以,但是一旦添加其他的类型,比如添加猫的类型进去,系统不会检查到错误
而是运行后才会报错。通过使用
1 | import java.util.ArrayList; |
改进后的:这样就限制了传入参数的类型,同时遍历的时候就不用进行向下转型了
1 | package generic.improve; |
###泛型的介绍
(1)泛型又称参数化类型,是Jdk5.0 出现的新特性,解决数据类型的安全性问题
(2)在类声明或实例化时只要指定好需要的具体的类型即可。
(3)Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。同时,代码更加简洁、健壮泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型或者是某个方
(4)法的返回值的类型,或者是参数类型
1 | //泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型, |
泛型的语法
- 泛型的声明:
interface 接□{} 和 class 类<K,V>{}
//比如:List , ArrayList
(1)其中,T,K,V不代表值,而是表示类型,
(2)任意字母都可以。常用T表示,是Type的缩写 - 泛型的实例化:
要在类名后面指定类型参数的值(类型)。如:
(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 | class 类名<T, R, ...> { // T、R 为泛型参数,可多个,用逗号分隔 |
注意细节
普通成员可使用泛型
类中的属性、方法(非静态)可以使用类声明的泛型参数。
示例:1
2
3
4
5
6
7
8
9
10
11class GenericClass<T> {
private T value; // 泛型属性
public T getValue() { // 泛型方法返回值
return value;
}
public void setValue(T value) { // 泛型方法参数
this.value = value;
}
}使用泛型的数组不能直接初始化
泛型数组在声明时可以使用泛型,但不能直接创建泛型数组的实例(因泛型类型在编译时擦除,无法确定具体类型)。
示例:1
2
3
4class GenericClass<T> {
private T[] arr; // 允许声明泛型数组引用
// private T[] arr = new T[10]; // 错误!不能直接初始化
}静态方法中不能使用类的泛型
静态成员(方法/属性)属于类级别,而泛型类的类型参数在创建对象时才确定,两者生命周期不匹配。
示例:1
2
3
4class GenericClass<T> {
// public static T staticValue; // 错误!静态属性不能用类泛型
// public static T getStaticValue() { return null; } // 错误!静态方法不能用类泛型
}泛型类的类型在创建对象时确定
创建泛型类对象时,必须指定具体的类型(除非使用菱形语法省略,编译器会自动推断)。
示例:1
2GenericClass<String> strObj = new GenericClass<>(); // 确定类型为 String
GenericClass<Integer> intObj = new GenericClass<>(); // 确定类型为 Integer未指定类型时默认为 Object
若创建对象时不指定泛型类型,泛型参数会被擦除为Object,失去泛型的类型约束作用。
示例:1
2
3GenericClass obj = new GenericClass(); // 等价于 GenericClass<Object>
obj.setValue("test"); // 允许,因 Object 可接收任意类型
Object value = obj.getValue(); // 返回值为 Object 类型
1 | package generic.coustemGeneric; |
1 | // 1. 声明泛型类型为 <Double, String, Integer> 的对象g |
自定义泛型接口
基本语法
1 | interface 接口名 <T,R...>{ |
- 注意细节
接口中,静态成员不能使用泛型(和前面的一样的道理) - 泛型接口的类型在继承接口或者实现接口时确定
- 没有指定类型,默认为Object
自定义泛型方法
基本语法
1 | 修饰符 <T, R, ...> 返回类型 方法名(参数列表) { |
<T, R, ...>是泛型方法的标志,必须放在修饰符与返回类型之间,声明该方法的泛型参数。
注意细节
泛型方法的定义位置
泛型方法可以定义在普通类中,也可以定义在泛型类中。
示例: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);
}
}泛型方法的类型确定时机
泛型方法的具体类型在调用时确定(编译器根据传入的参数类型自动推断,或显式指定)。
示例:1
2
3NormalClass nc = new NormalClass();
String str = nc.getValue("test"); // 调用时确定T为String
Integer num = nc.getValue(123); // 调用时确定T为Integer区分“泛型方法”与“使用泛型的方法”
- 若方法没有
<T, R...>声明,即使使用了泛型参数(如类的泛型),也不是泛型方法,只是“使用了泛型的方法”。
示例:区别:泛型方法的泛型参数独立于类的泛型,而“使用泛型的方法”依赖于类的泛型类型。1
2
3
4
5
6
7
8
9
10
11class 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 | // 错误!泛型不具备继承性 |
原因:ArrayList<String> 不是 List<Object> 的子类,泛型的类型参数会在编译时进行严格检查,避免类型不安全的操作。
2. 泛型通配符(Wildcard)
通配符用于灵活限制泛型的类型范围,常见有以下三种形式:
(1)<?>:无界通配符
表示支持任意泛型类型,即可以匹配所有泛型实例。
示例:
1 | public void printList(List<?> list) { |
(2)<? extends A>:上界通配符
表示支持 A类及其所有子类 的泛型类型,限定了泛型的上限。
示例:
1 | class A {} |
注意:使用上界通配符的集合只能读取元素(可转型为A),不能添加元素(无法确定具体子类类型,可能导致类型混乱)。
(3)<? super A>:下界通配符
表示支持 A类及其所有父类 的泛型类型,限定了泛型的下限。
示例:
1 | class 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
70package 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
- Collection实现子类可以存放多个元素,每个元素可以是Object
- 有些Collection的实现类,可以存放重复的元素,有些不可以
- 有些Collection的实现类,有些是有序的(List),有些不是有序(Set)
- 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
33import java.util.ArrayList;
//抑制警告
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 | java.util |
- Iterator对象称为迭代器,主要用于遍历 Collection 集合中的元素。
- 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器。
- Iterator 的结构
- Iterator 仅用于遍历集合,Iterator 本身并不存放对象。
1 | package collection.homeWork; |
List
List接口和常用方法
List接口基本介绍
List 接口是 Collection 接口的子接口
- List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
- List集合中的每个元素都有其对应的顺序索引,即支持索引
- List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号获取容器中的元素
- JDK API中List接口的实现类有:
常用的有:ArrayList、LinkedList和Vector
示例代码
1 | List list = new ArrayList(); |
1 | package collection.homeWork; |
常用方法
List的三种遍历方式ArrayList,linkedList,Vector这些都适用这些遍历
1 | import java.util.ArrayList; |
1 | import java.util.ArrayList; |
vector
Vector和ArrayList一样,都是Vector的子类,Vector是线程安全的,ArrayList是线程不安全的。
LinkedList和ArryList在实际开发中的选择
在实际开发中,如果改查的情况比较多的情况下选择ArrayList(线程不安全)
如果增删的情况比较多,选择LinkedList(线程不安全)
Set
Set是无序的,没有索引,不允许重复
Set接口和常用方法
和list接口一样,Set接口也是Collection的子接口,因此,常用方法和Collection接口一样
Set接口的遍历方式
- 可以使用迭代器
- 增强for
不能使用索引的方式来获取
数组链表模拟
一个节点中存放了两个信息,第一个是存放的是元素的值item,第二个存放的是指向下一个节点的地址。
我们用一个node类来模拟一个节点
1 | package collection.Set; |
List、Set 和 Map 什么时候用?它们各自适合什么场景?
我们来结合你提供的这张图,详细解释一下这三者的区别和使用场景。
🔹 一、先看结构图的含义
你的图展示了 Java 集合框架的继承关系:
1 | Iterable |
Iterable:所有可遍历的集合都实现这个接口。Collection:是所有单值集合的父接口,包括 List 和 Set。List:允许重复元素,有索引(有序)。Set:不允许重复元素(无序或有序,取决于实现)。Map:不在图中,但它是另一个分支,存储“键值对”。
⚠️ 注意:
Map不继承自Collection,它是一个独立的接口。
✅ 二、List、Set、Map 的核心区别
| 类型 | 是否允许重复 | 是否有序 | 存储方式 | 主要用途 |
|---|---|---|---|---|
| List | ✅ 允许 | ✅ 有序(按插入顺序) | 索引访问 | 按顺序保存数据,支持重复项 |
| Set | ❌ 不允许 | ❌ 无序(HashSet) / ✅ 有序(TreeSet) | 唯一性保证 | 去重、唯一集合 |
| Map | ✅ 键唯一,值可重复 | ❌ 键有序/无序(取决于实现) | 键值对 | 快速查找、关联数据 |
✅ 三、具体使用场景详解
🟦 1. 使用 List 的情况
当你需要:
- 保留元素的插入顺序;
- 允许重复元素;
- 通过索引快速访问元素(如
list.get(0));
✅ 举个例子:
1 | List<String> names = new ArrayList<>(); |
📌 常见使用场景:
- 存储用户列表、订单列表、日志记录等;
- 需要频繁增删改查的有序数据;
- 需要随机访问元素时(ArrayList 比较快);
🔹 推荐选择:
ArrayList:查询快,适合读多写少;LinkedList:插入删除快,适合频繁增删;
🟨 2. 使用 Set 的情况
当你需要:
- 去重;
- 不关心顺序(或要求排序);
- 快速判断某个元素是否存在;
✅ 举个例子:
1 | Set<String> uniqueNames = new HashSet<>(); |
📌 常见使用场景:
- 统计不重复的用户 ID;
- 过滤重复数据;
- 实现“黑名单”、“白名单”;
- 需要快速判断某元素是否存在的场景;
🔹 推荐选择:
HashSet:无序,性能最好(O(1) 查找);TreeSet:有序(自然排序),但性能稍慢(O(log n));
🟩 3. 使用 Map 的情况
当你需要:
- 存储“键 → 值”关系;
- 根据键快速查找对应的值;
- 键必须唯一,值可以重复;
✅ 举个例子:
1 | Map<String, Integer> scores = new HashMap<>(); |
📌 常见使用场景:
- 用户信息管理(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 | // 1. k-v 最后是 HashMap$Node node = newNode(hash, key, value, null) |
1 | import java.util.HashMap; |
Map接口和常用方法
- put: 添加
- remove: 根据键删除映射关系
- get: 根据键获取值
- size: 获取元素个数
- isEmpty: 判断个数是否为0
- clear: 清除
- 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
51package Map.HashMap;
import java.util.HashMap;
import java.util.Map;
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"));
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
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.*;
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.*;
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());
}
}
}
}
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;
}
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 | import java.util.ArrayList; |













