2.1 类与对象
核心概念
类是对象的模板,对象是类的实例。类定义了对象的属性(成员变量)和行为(成员方法),而对象是类的具体化表现。
定义语法
// 定义类
[访问修饰符] class 类名 {
// 成员变量(属性)
数据类型 变量名;
// 成员方法(行为)
返回类型 方法名(参数列表) {
// 方法体
}
}实例化语法
类名 对象名 = new 类名();完整示例
// 定义一个学生类
public class Student {
// 成员变量
String name;
int age;
double score;
// 成员方法
public void study() {
System.out.println(name + " 正在学习");
}
public void introduce() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁,成绩是 " + score);
}
}
// 使用类创建对象
public class Main {
public static void main(String[] args) {
// 实例化:创建对象
Student s1 = new Student();
Student s2 = new Student();
// 为对象赋值
s1.name = "张三";
s1.age = 18;
s1.score = 92.5;
s2.name = "李四";
s2.age = 19;
s2.score = 88.0;
// 调用对象方法
s1.study(); // 输出:张三 正在学习
s1.introduce(); // 输出:我叫 张三,今年 18 岁,成绩是 92.5
s2.introduce(); // 输出:我叫 李四,今年 19 岁,成绩是 88.0
}
}内存分析
- 栈内存:存储局部变量和对象引用(引用变量名)。
- 堆内存:存储
new出来的对象实体(成员变量值)。 - 元空间:静态变量存储在堆中,类元数据(方法、字段描述等)存储在元空间
Student s1 = new Student(); // s1 在栈中,new Student() 在堆中注意事项
- 一个 Java 文件中可以有多个类,但最多只有一个
public修饰的类,且文件名必须与public类名一致。 - 对象的默认值:数值类型为
0/0.0,boolean为false,引用类型为null。 - 使用
new关键字的过程:加载类 → 在堆中分配内存 → 初始化默认值 → 执行构造方法 → 返回引用地址。
2.2 方法
定义语法
[访问修饰符] [static] [final] 返回值类型 方法名(参数类型 参数名, ...) {
// 方法体
[return 返回值;]
}完整示例
public class Calculator {
// 实例方法:需要对象调用
public int add(int a, int b) {
return a + b;
}
// 静态方法:通过类名直接调用
public static int subtract(int a, int b) {
return a - b;
}
// 无返回值方法
public void printResult(String operation, int result) {
System.out.println(operation + " 结果:" + result);
}
}
// 调用示例
public class Main {
public static void main(String[] args) {
// 静态方法:类名调用
int diff = Calculator.subtract(10, 3);
// 实例方法:对象调用
Calculator calc = new Calculator();
int sum = calc.add(10, 3);
calc.printResult("加法", sum);
}
}静态方法 vs 实例方法对比
| 对比维度 | 静态方法 (static) | 实例方法 (非static) |
|---|---|---|
| 修饰符 | 使用 static 修饰 | 不使用 static |
| 调用方式 | 类名直接调用 ClassName.method() | 必须通过对象调用 obj.method() |
| 内存位置 | 方法区(就是静态的代码方法而已) | 元空间(就是静态的代码方法而已) |
| 访问实例成员 | 不能直接访问实例变量和实例方法 | 可以访问所有成员(静态 + 实例) |
| 访问静态成员 | 可以访问(推荐用类名访问) | 可以访问 |
this / super | 不能使用 | 可以使用 |
| 应用场景 | 工具方法(如 Math.pow())、独立功能 | 与对象状态相关的方法 |
参数传递详解
Java 中所有参数传递都是值传递,但理解基本类型和引用类型的区别至关重要。
基本类型传递(传递副本,不改变原值)
public class ParamTest {
public static void changeValue(int x) {
x = 100; // 修改的是副本,不影响原值
System.out.println("方法内 x = " + x);
}
public static void main(String[] args) {
int a = 10;
changeValue(a);
System.out.println("方法外 a = " + a); // 输出 10,原值不变
}
}引用类型传递(传递引用副本,可修改对象内容但不能改变引用指向)
public class ParamTest {
public static void modifyObject(StringBuilder sb) {
sb.append(" World"); // 可以修改对象内容
System.out.println("方法内 sb = " + sb);
}
public static void changeReference(StringBuilder sb) {
sb = new StringBuilder("New Object"); // 不能改变引用指向
System.out.println("方法内 sb = " + sb);
}
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
modifyObject(sb);
System.out.println("方法外 sb = " + sb); // 输出 Hello World,内容已改变
changeReference(sb);
System.out.println("方法外 sb = " + sb); // 仍输出 Hello World,引用未改变
}
}可变参数(varargs)
// 可变参数:参数类型... 参数名(本质是数组)
public static int sum(int... numbers) {
int total = 0;
for (int n : numbers) {
total += n;
}
return total;
}
// 调用
int result = sum(1, 2, 3, 4, 5); // result = 15
int result2 = sum(); // result2 = 0注意:可变参数必须是参数列表中的最后一个参数,一个方法最多只能有一个可变参数。
方法设计原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 单一职责 | 每个方法只做一件事,职责明确 | calculateTotal() 不应同时打印和计算 |
| 命名清晰 | 方法名使用动词开头,见名知义 | getUserById() 优于 get() |
| 参数不超过5个 | 参数过多应封装为对象 | 使用 DTO / POJO 封装多参数 |
| 方法体不超过50行 | 保持方法简短易读 | 超出则拆分子方法 |
| 无副作用 | 尽量避免修改传入参数 | 返回新值而非修改参数 |
方法命名规范
- 获取值:
getXxx()/findXxx()/queryXxx() - 设置值:
setXxx()/updateXxx() - 判断:
isXxx()/hasXxx()/canXxx() - 转换:
toXxx()/asXxx() - 创建:
createXxx()/buildXxx()/newXxx() - 处理:
processXxx()/handleXxx()/executeXxx()
2.3 构造方法
核心概念
构造方法是一种特殊的方法,用于在创建对象时初始化对象的状态。
构造方法特征
- 方法名必须与类名完全相同(包括大小写)
- 没有返回类型,连
void也不能写 - 使用
new关键字自动调用,不能像普通方法一样显式调用 - 每个类至少有一个构造方法
基本用法
public class Person {
private String name;
private int age;
// 无参构造方法(默认构造方法)
public Person() {
System.out.println("Person 无参构造被调用");
}
// 有参构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("Person 有参构造被调用,name = " + name);
}
// 多个重载的构造方法
public Person(String name) {
this.name = name;
this.age = 0; // 默认值
}
}重要规则
一旦手动定义了有参构造方法,编译器将不再自动提供默认的无参构造方法。如果需要无参构造,必须显式定义。
public class Student {
private String name;
// 只定义了有参构造
public Student(String name) {
this.name = name;
}
}
// 以下代码将编译错误!
// Student s = new Student(); // 错误:无参构造不存在链式调用 this()
this() 用于在一个构造方法中调用本类的另一个构造方法,实现代码复用。
public class Employee {
private String name;
private int age;
private String department;
// 构造方法1:全参
public Employee(String name, int age, String department) {
this.name = name;
this.age = age;
this.department = department;
}
// 构造方法2:只有姓名和年龄
public Employee(String name, int age) {
this(name, age, "未分配"); // 调用构造方法1
}
// 构造方法3:只有姓名
public Employee(String name) {
this(name, 0, "未分配"); // 调用构造方法1
}
// 构造方法4:无参
public Employee() {
this("未知", 0, "未分配"); // 调用构造方法1
}
}this() 使用规则
this()必须是构造方法的第一条语句。- 一个构造方法中最多只能出现一次
this()调用。 - 不能形成循环调用,否则编译错误。
// 错误示例:循环调用
public class A {
public A() {
this(10); // 调用有参构造
}
public A(int x) {
this(); // 调用无参构造 → 形成循环,编译错误!
}
}Builder 模式
当类的属性过多时,构造方法的参数列表会变得冗长且难以阅读。Builder 模式是解决此问题的优雅方案。
public class Computer {
// 必选参数
private final String cpu;
private final String ram;
// 可选参数
private final String gpu;
private final String storage;
private final String os;
// 私有构造方法,仅 Builder 可调用
private Computer(Builder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.gpu = builder.gpu;
this.storage = builder.storage;
this.os = builder.os;
}
// 静态内部 Builder 类
public static class Builder {
// 必选参数
private final String cpu;
private final String ram;
// 可选参数(默认值)
private String gpu = "集成显卡";
private String storage = "256GB SSD";
private String os = "Windows 11";
// Builder 构造方法:必须提供必选参数
public Builder(String cpu, String ram) {
this.cpu = cpu;
this.ram = ram;
}
public Builder gpu(String gpu) {
this.gpu = gpu;
return this;
}
public Builder storage(String storage) {
this.storage = storage;
return this;
}
public Builder os(String os) {
this.os = os;
return this;
}
public Computer build() {
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer {cpu=" + cpu + ", ram=" + ram +
", gpu=" + gpu + ", storage=" + storage + ", os=" + os + "}";
}
}
// 使用 Builder 模式创建对象
public class Main {
public static void main(String[] args) {
Computer pc1 = new Computer.Builder("Intel i7", "16GB")
.build();
Computer pc2 = new Computer.Builder("AMD Ryzen 9", "32GB")
.gpu("NVIDIA RTX 4080")
.storage("1TB NVMe SSD")
.os("Linux")
.build();
System.out.println(pc1);
System.out.println(pc2);
}
}构造方法设计原则
| 原则 | 说明 |
|---|---|
| 参数验证 | 在构造方法中对参数进行合法性校验,确保对象创建后处于有效状态 |
| 链式调用 | 利用 this() 减少重复代码,保持构造逻辑集中 |
| 防御性复制 | 对于可变引用类型参数,创建防御性副本,防止外部修改影响内部状态 |
| 保持简洁 | 构造方法应只做初始化工作,避免复杂业务逻辑 |
| 不调用可重写方法 | 构造方法中调用可被重写的方法会导致不可预期的行为(子类可能尚未初始化完毕) |
// 参数验证 + 防御性复制示例
public class Order {
private final List<String> items;
private final Date createTime;
public Order(List<String> items) {
// 参数验证
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("订单项不能为空");
}
// 防御性复制
this.items = new ArrayList<>(items);
this.createTime = new Date();
}
public List<String> getItems() {
// 返回防御性副本
return new ArrayList<>(items);
}
}2.4 访问修饰符
四种访问级别
Java 提供四种访问修饰符来控制类、成员变量、成员方法和构造方法的可见性。
| 修饰符 | 同类 | 同包 | 子类(不同包) | 所有类 |
|---|---|---|---|---|
private | 可访问 | 不可访问 | 不可访问 | 不可访问 |
default(无修饰符) | 可访问 | 可访问 | 不可访问 | 不可访问 |
protected | 可访问 | 可访问 | 可访问 | 不可访问 |
public | 可访问 | 可访问 | 可访问 | 可访问 |
代码示例
package com.example.base;
public class Parent {
private int privateField = 1; // 仅本类内访问
int defaultField = 2; // 同包访问(default)
protected int protectedField = 3; // 同包 + 子类访问
public int publicField = 4; // 所有类访问
public void testAccess() {
System.out.println(privateField); // 可访问(同类内)
System.out.println(defaultField); // 可访问
System.out.println(protectedField); // 可访问
System.out.println(publicField); // 可访问
}
}package com.example.base;
public class SamePackageClass {
public void testAccess() {
Parent p = new Parent();
// System.out.println(p.privateField); // 编译错误:不同类
System.out.println(p.defaultField); // 可访问(同包)
System.out.println(p.protectedField); // 可访问(同包)
System.out.println(p.publicField); // 可访问
}
}package com.example.other;
import com.example.base.Parent;
public class Child extends Parent {
public void testAccess() {
// System.out.println(privateField); // 编译错误
// System.out.println(defaultField); // 编译错误:不同包
System.out.println(protectedField); // 可访问(子类)
System.out.println(publicField); // 可访问
}
}package com.example.other;
import com.example.base.Parent;
public class UnrelatedClass {
public void testAccess() {
Parent p = new Parent();
// System.out.println(p.privateField); // 编译错误
// System.out.println(p.defaultField); // 编译错误
// System.out.println(p.protectedField);// 编译错误:不同包非子类
System.out.println(p.publicField); // 可访问
}
}访问修饰符可修饰的元素
| 修饰的元素 | private | default | protected | public |
|---|---|---|---|---|
| 外部类 | 不支持 | 支持 | 不支持 | 支持 |
| 内部类 | 支持 | 支持 | 支持 | 支持 |
| 成员变量 | 支持 | 支持 | 支持 | 支持 |
| 成员方法 | 支持 | 支持 | 支持 | 支持 |
| 构造方法 | 支持 | 支持 | 支持 | 支持 |
设计原则
- 封装核心:隐藏内部实现细节,只暴露必要的公共接口。
- 最小权限原则:能用
private就不用default,能用default就不用protected,能用protected就不用public。 - 字段私有:所有成员变量应使用
private修饰,通过getter/setter方法访问。 - 常量公开:公共常量使用
public static final。 - 内部辅助方法:仅供类内部使用的方法使用
private。
// 良好的封装示例
public class BankAccount {
private String accountNumber; // 私有的内部数据
private double balance;
// 公共的访问接口
public String getAccountNumber() {
return accountNumber;
}
public double getBalance() {
return balance;
}
// 存款:公共方法,但内部有验证
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("存款金额必须大于0");
}
balance += amount;
}
// 内部使用的私有辅助方法
private void validateAmount(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("金额无效");
}
}
}2.5 this 关键字
核心概念
this 是 Java 中的关键字,表示对当前对象的引用。每个实例方法中都有一个隐含的 this 引用指向调用该方法的对象。
三大用途
1. 引用成员变量(区分同名的局部变量和成员变量)
当方法参数名与成员变量名相同时,必须使用 this 来区分。
public class Person {
private String name;
private int age;
// 必须使用 this 区分
public Person(String name, int age) {
this.name = name; // this.name 是成员变量,name 是参数
this.age = age; // this.age 是成员变量,age 是参数
}
public void setName(String name) {
this.name = name; // 参数与成员变量同名时必须使用 this
}
// 不同名时可以省略 this(但不推荐,降低可读性)
public void setAge(int newAge) {
age = newAge; // 可以省略 this
// this.age = newAge; // 推荐显式使用 this
}
}2. 调用本类的其他构造方法 this()
public class Student {
private String name;
private int age;
private String grade;
public Student() {
this("未知", 0, "未分配"); // 调用三参构造方法
}
public Student(String name) {
this(name, 0, "未分配"); // 调用三参构造方法
}
public Student(String name, int age) {
this(name, age, "未分配"); // 调用三参构造方法
}
// 最终的构造方法(入口)
public Student(String name, int age, String grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
}3. 方法链式调用(Fluent API)
返回 this 可以实现链式调用,使代码更简洁流畅。
public class QueryBuilder {
private String table;
private List<String> columns = new ArrayList<>();
private String whereClause;
private String orderBy;
public QueryBuilder select(String... cols) {
columns.addAll(Arrays.asList(cols));
return this; // 返回当前对象,支持链式调用
}
public QueryBuilder from(String table) {
this.table = table;
return this;
}
public QueryBuilder where(String condition) {
this.whereClause = condition;
return this;
}
public QueryBuilder orderBy(String field) {
this.orderBy = field;
return this;
}
public String build() {
// 构建 SQL 语句...
return "SELECT " + String.join(", ", columns) +
" FROM " + table +
(whereClause != null ? " WHERE " + whereClause : "") +
(orderBy != null ? " ORDER BY " + orderBy : "");
}
// 使用示例
public static void main(String[] args) {
QueryBuilder qb = new QueryBuilder();
String sql = qb.select("id", "name", "age")
.from("users")
.where("age > 18")
.orderBy("name")
.build();
System.out.println(sql);
// 输出:SELECT id, name, age FROM users WHERE age > 18 ORDER BY name
}
}this 使用限制
| 限制 | 说明 | 示例 |
|---|---|---|
| 静态方法中不能使用 | 静态方法属于类,没有对象上下文,不存在 this | static void method() { this.xxx; } ❌ |
| 静态代码块中不能使用 | 静态代码块在类加载时执行,此时没有对象实例 | static { this.xxx; } ❌ |
| this() 必须是构造方法第一条语句 | 构造方法调用必须在最前面 | public A() { this("default"); } |
使用场景总结
| 场景 | 是否使用 this | 说明 |
|---|---|---|
| 成员变量与局部变量同名 | 必须使用 | this.name = name; |
| 成员变量与局部变量不同名 | 推荐使用 | 提高可读性,明确意图 |
| 构造方法间相互调用 | 必须使用 this() | 第一条语句 |
| 链式调用 | 必须使用 return this | Fluent API 模式 |
| 将当前对象作为参数传递 | 使用 this | method(this) |
| 静态上下文中 | 禁止使用 | 编译错误 |
2.6 static 关键字
核心概念
static 表示"静态的"、"属于类的",被 static 修饰的成员不属于任何一个对象实例,而是属于类本身。
静态变量(类变量)
静态变量属于类,被该类的所有对象共享,其生命周期与类的生命周期一致。
public class Counter {
// 静态变量:所有 Counter 对象共享
private static int totalCount = 0;
// 实例变量:每个对象独立拥有
private int instanceId;
public Counter() {
totalCount++; // 每次创建对象,计数 +1
instanceId = totalCount; // 将总数作为当前ID
}
public static int getTotalCount() {
return totalCount;
}
public int getInstanceId() {
return instanceId;
}
public static void main(String[] args) {
Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();
System.out.println(c1.getInstanceId()); // 输出: 1
System.out.println(c2.getInstanceId()); // 输出: 2
System.out.println(c3.getInstanceId()); // 输出: 3
System.out.println(Counter.getTotalCount());// 输出: 3(所有对象共享)
// 三个对象共享同一个 totalCount
System.out.println(c1.getTotalCount()); // 输出: 3
System.out.println(c2.getTotalCount()); // 输出: 3
}
}静态变量的内存与生命周期
| 阶段 | 说明 |
|---|---|
| 加载 | 类被 JVM 加载时,静态变量在方法区分配空间并初始化 |
| 使用 | 可通过 类名.变量名 或 对象.变量名 访问(推荐类名方式) |
| 卸载 | 类被卸载时,静态变量占用的内存释放 |
静态方法(重点)
类名.静态变量 | 变量引用在堆(附着于Class对象) | 类加载时初始化,类卸载(或JVM退出)时销毁 |
|---|---|---|
类名.静态方法() | 代码在元空间,执行时的临时数据在栈 | 类加载后即可调用,调用结束栈帧即销毁,随类卸载而消失 |
静态方法属于类,只能直接访问静态成员(静态变量和静态方法),不能直接访问实例成员。
public class MathUtils {
// 静态常量
public static final double PI = 3.1415926535;
// 静态方法:不依赖对象状态
public static double squareArea(double side) {
return side * side;
}
public static double circleArea(double radius) {
return PI * radius * radius;
}
// 实例方法:可以访问静态成员
public void printPI() {
System.out.println("PI = " + PI); // 实例方法访问静态变量,OK
System.out.println("面积 = " + squareArea(5)); // OK
}
// 错误示例:静态方法访问实例成员
// public static void errorMethod() {
// printPI(); // 编译错误!静态方法不能调用实例方法
// }
}
// 使用
public class Main {
public static void main(String[] args) {
// 直接通过类名调用
double area = MathUtils.circleArea(5.0);
System.out.println("圆面积:" + area);
}
}静态方法不能被重写,只能被隐藏
class Parent {
public static void staticMethod() {
System.out.println("Parent 静态方法");
}
public void instanceMethod() {
System.out.println("Parent 实例方法");
}
}
class Child extends Parent {
// 这不是重写(Override),而是隐藏(Hide)
public static void staticMethod() {
System.out.println("Child 静态方法");
}
// 这是真正的重写
@Override
public void instanceMethod() {
System.out.println("Child 实例方法");
}
}
public class Test {
public static void main(String[] args) {
Parent p = new Child();
p.staticMethod(); // 输出:Parent 静态方法(基于引用类型,不具有多态性)
p.instanceMethod(); // 输出:Child 实例方法(基于实际对象类型,多态性)
Child.staticMethod(); // 输出:Child 静态方法(调用子类的隐藏版本)
}
}静态代码块
静态代码块在类加载时执行,且只执行一次,常用于初始化静态资源。
public class DatabaseConfig {
// 静态变量
private static String url;
private static String username;
private static String password;
// 静态代码块:类加载时只执行一次
static {
System.out.println("正在加载数据库配置...");
// 模拟从配置文件加载
url = "jdbc:mysql://localhost:3306/mydb";
username = "root";
password = "123456";
System.out.println("数据库配置加载完成");
}
// 静态代码块可以有多个,按声明顺序执行
static {
System.out.println("第二个静态代码块执行");
}
public static void connect() {
System.out.println("连接数据库:" + url);
}
public static void main(String[] args) {
DatabaseConfig.connect();
// 输出:
// 正在加载数据库配置...
// 数据库配置加载完成
// 第二个静态代码块执行
// 连接数据库:jdbc:mysql://localhost:3306/mydb
}
}初始化顺序总结
当类被加载和实例化时,各部分的初始化顺序如下:
1. 父类静态变量和静态代码块(按声明顺序)
2. 子类静态变量和静态代码块(按声明顺序)
3. 父类实例变量和实例代码块(按声明顺序)
4. 父类构造方法
5. 子类实例变量和实例代码块(按声明顺序)
6. 子类构造方法class Parent {
static { System.out.println("1. Parent 静态代码块"); }
{ System.out.println("3. Parent 实例代码块"); }
Parent() { System.out.println("4. Parent 构造方法"); }
}
class Child extends Parent {
static { System.out.println("2. Child 静态代码块"); }
{ System.out.println("5. Child 实例代码块"); }
Child() { System.out.println("6. Child 构造方法"); }
public static void main(String[] args) {
new Child();
}
}2.7 继承
核心概念
继承是面向对象编程的核心特性之一,允许一个类(子类)继承另一个类(父类)的属性和方法。
基本语法
[访问修饰符] class 子类名 extends 父类名 {
// 子类特有的属性和方法
}基本示例
// 父类(基类 / 超类)
public class Animal {
protected String name;
protected int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println(name + " 正在吃东西");
}
public void sleep() {
System.out.println(name + " 正在睡觉");
}
}
// 子类(派生类)
public class Dog extends Animal {
private String breed;
public Dog(String name, int age, String breed) {
super(name, age); // 调用父类构造方法
this.breed = breed;
}
// 子类特有方法
public void bark() {
System.out.println(name + " 汪汪叫!");
}
// 重写父类方法
@Override
public void eat() {
System.out.println(name + "(" + breed + ")正在吃狗粮");
}
}
// Cat 子类
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name + " 正在吃鱼");
}
public void meow() {
System.out.println(name + " 喵喵叫!");
}
}
// 使用
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("旺财", 3, "金毛");
Cat cat = new Cat("咪咪", 2);
dog.eat(); // 输出:旺财(金毛)正在吃狗粮
dog.bark(); // 输出:旺财 汪汪叫!
dog.sleep(); // 输出:旺财 正在睡觉(继承自 Animal)
cat.eat(); // 输出:咪咪 正在吃鱼
cat.meow(); // 输出:咪咪 喵喵叫!
cat.sleep(); // 输出:咪咪 正在睡觉(继承自 Animal)
}
}Java 继承规则
| 规则 | 说明 |
|---|---|
| 单继承 | 一个类只能有一个直接父类(extends 后只能跟一个类名) |
| 多层继承 | 可以形成继承链:A extends B, B extends C(层次通常不超过 3-4 层) |
| 隐式继承 Object | 所有类如果没有显式继承,则隐式继承 java.lang.Object |
| 不继承构造方法 | 子类不继承父类的构造方法,但可以通过 super() 调用 |
| 继承所有非私有成员 | 子类继承父类的 public、protected、default 成员(同包情况下) |
is-a 关系判断
继承表示 "is-a"(是一个)关系。判断是否适合使用继承的关键标准:
// 好的继承:Dog IS-A Animal — 合理
class Dog extends Animal // 狗是一种动物
// 好的继承:Car IS-A Vehicle — 合理
class Car extends Vehicle // 汽车是一种交通工具
// 不好的继承:为了复用代码而继承
class Stack extends ArrayList // 栈不是一个列表,违反了 is-a
// 应该使用组合:
class Stack {
private ArrayList list = new ArrayList(); // 组合优于继承
}继承的设计原则
推荐做法
// 1. 父类设计考虑扩展性,使用 protected 提供子类访问
public class BaseService {
protected Logger logger = LoggerFactory.getLogger(getClass());
// 模板方法模式:定义框架,子类实现细节
public final void execute() {
beforeExecute();
doExecute();
afterExecute();
}
protected void beforeExecute() // 钩子方法
protected abstract void doExecute(); // 子类必须实现
protected void afterExecute() // 钩子方法
}
// 2. 使用 @Override 注解防止错误
class ChildService extends BaseService {
@Override // 编译器会检查是否正确重写,防止拼写错误
protected void doExecute() {
logger.info("执行业务逻辑");
}
}
// 3. 子类构造调用 super()
class Child extends Parent {
public Child(String name) {
super(name); // 必须调用父类构造方法,确保父类正确初始化
}
}避免的做法
| 避免的做法 | 原因 | 正确做法 |
|---|---|---|
| 为代码复用强行继承 | 违反 is-a 原则,导致耦合 | 使用组合 + 委托 |
| 在构造方法中调用可重写方法 | 子类方法可能在父类未完全初始化时被调用 | 构造方法只调用 private / final / static 方法 |
| 随意修改父类 | 违反开闭原则 | 通过扩展(新增子类)而非修改父类来增加功能 |
// 违反开闭原则的反例
// 不应该为新增功能而修改稳定的父类
// 正确做法:使用组合替代不当的继承
// 需要复用某功能,但不符合 is-a 关系时
class ReportGenerator {
// 组合:持有需要的对象
private DataFetcher dataFetcher;
private Formatter formatter;
public ReportGenerator(DataFetcher dataFetcher, Formatter formatter) {
this.dataFetcher = dataFetcher;
this.formatter = formatter;
}
}2.8 super 关键字
核心概念
super 是 Java 中指向直接父类对象引用的关键字,用于在子类中访问父类的成员(构造方法、方法、属性)。
三大用途
1. 调用父类构造方法 super()
class Parent {
protected String name;
// 父类有参构造
public Parent(String name) {
this.name = name;
System.out.println("Parent 构造:name = " + name);
}
}
class Child extends Parent {
private int age;
public Child(String name, int age) {
super(name); // 必须显式调用父类有参构造
this.age = age;
System.out.println("Child 构造:age = " + age);
}
}
public class Main {
public static void main(String[] args) {
Child c = new Child("小明", 10);
// 输出:
// Parent 构造:name = 小明
// Child 构造:age = 10
}
}2. 访问父类被重写的方法
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
super.makeSound(); // 调用父类的方法
System.out.println("狗:汪汪!"); // 扩展父类方法
}
}
// 使用
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.makeSound();
// 输出:
// 动物发出声音
// 狗:汪汪!
}
}3. 访问父类被隐藏的属性
class Parent {
protected String className = "Parent";
protected int value = 100;
}
class Child extends Parent {
protected String className = "Child"; // 隐藏父类同名属性
public void printInfo() {
System.out.println("子类的 className: " + this.className); // Child
System.out.println("父类的 className: " + super.className); // Parent
System.out.println("value: " + super.value); // 100(继承的)
}
}super 使用规则
| 规则 | 说明 |
|---|---|
| super() 必须是第一条语句 | super() 调用必须放在构造方法体的第一行 |
| 不显式调用自动插入 | 如果子类构造方法没有显式调用 super(),编译器会自动插入父类的无参构造 super() |
| 父类无无参构造 | 如果父类没有无参构造方法,子类必须显式通过 super(参数) 调用父类的有参构造,否则编译错误 |
| 不能在静态方法中使用 | super 是对象级别的引用,静态上下文中不存在 |
// 错误示例:父类无无参构造但子类未显式调用
class Parent {
public Parent(int x) // 只有有参构造
}
// class Child extends Parent {
// // 编译错误!隐式的 super() 找不到 Parent 的无参构造
// }
// 正确示例
class Child extends Parent {
public Child(int x) {
super(x); // 必须显式调用
}
}super vs this 对比
| 对比维度 | super | this |
|---|---|---|
| 访问范围 | 访问父类的成员(方法、属性) | 访问当前类的成员(方法、属性) |
| 调用构造方法 | super() 调用父类的构造方法 | this() 调用本类的其他构造方法 |
| 本质 | 对直接父类的引用 | 对当前对象的引用 |
| 内存指向 | 指向父类部分的内存区域 | 指向当前对象整体 |
| 静态方法中 | 不能使用 | 不能使用 |
| 第一条语句限制 | super() 必须是构造方法第一条语句 | this() 必须是构造方法第一条语句 |
| 能否共存 | 不能与 this() 同时作为第一条语句 | 不能与 super() 同时作为第一条语句 |
2.9 final 关键字
核心概念
final 表示"最终的、不可改变的",可以修饰类、方法和变量。
final 类:不能被继承
当一个类被 final 修饰时,它不能被任何类继承。
// final 类:不可被继承
public final class StringUtils {
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
}
// 以下代码编译错误!
// class ExtendedStringUtils extends StringUtils // 编译错误!典型 final 类:java.lang.String、java.lang.Math、java.lang.Integer 等包装类。
final 方法:不能被重写,但能被继承调用
class Parent {
// final 方法:子类不能重写
public final void criticalMethod() {
System.out.println("核心方法,不能被子类改变");
}
// 普通方法:可以被子类重写
public void normalMethod() {
System.out.println("普通方法");
}
}
class Child extends Parent {
// @Override
// public void criticalMethod() // 编译错误!final 方法不能重写
@Override
public void normalMethod() {
System.out.println("子类重写的普通方法");
}
// 可以继承并调用父类的 final 方法
public void useParentMethod() {
criticalMethod(); // 调用继承的 final 方法,OK
}
}final 变量:赋值后不能修改
final 变量根据声明位置的不同,初始化时机也有所不同。
public class FinalVariableDemo {
// 1. 静态 final 变量(类常量):声明时或静态代码块中初始化
public static final double PI = 3.14159;
public static final String APP_NAME;
static {
APP_NAME = "MyApplication"; // 静态代码块中初始化
}
// 2. 实例 final 变量:声明时或构造方法中初始化
private final String id; // 声明时未赋值
private final String name = "默认名称"; // 声明时赋值
public FinalVariableDemo(String id) {
this.id = id; // 构造方法中初始化
}
// 3. 局部 final 变量:使用前初始化一次
public void process() {
final int localValue;
localValue = 100; // 首次赋值,OK
// localValue = 200; // 编译错误!不能二次赋值
}
// final 引用类型:引用不可变,但对象内容可变
public void finalReference() {
final StringBuilder sb = new StringBuilder("Hello");
sb.append(" World"); // OK:可以修改对象内容
// sb = new StringBuilder("New"); // 编译错误!不能改变引用指向
}
}final 变量初始化规则总结
| final 变量类型 | 必须初始化的时机 | 示例 |
|---|---|---|
| 静态 final 变量 | 声明时 或 静态代码块中 | static final int X = 10; 或 static { X = 10; } |
| 实例 final 变量 | 声明时 或 每个构造方法中 | final int x = 10; 或构造方法中 this.x = 10; |
| 局部 final 变量 | 使用前赋值一次即可 | final int x; x = 10; use(x); |
final 方法参数
public class FinalParameter {
// final 参数:方法调用时赋值,方法体内不可修改
public void printName(final String name) {
// name = "新的名字"; // 编译错误!不能修改 final 参数
System.out.println("姓名:" + name);
}
// final 参数在匿名内部类中很有用
public void doSomething(final String message) {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println(message); // 匿名内部类可以访问 final 变量
}
};
new Thread(r).start();
}
}final 使用场景
| 场景 | 说明 | 示例 |
|---|---|---|
| 定义常量 | public static final 定义公共常量 | Math.PI |
| 防止继承 | 工具类、不可变类 | String、Integer |
| 防止重写 | 模板方法中的框架方法 | 流程控制方法 |
| 安全发布 | 确保对象在构造函数中完全初始化 | 多线程环境下的不可变对象 |
| 匿名内部类 | 匿名内部类访问的局部变量 | final 局部变量在 Lambda 中 |
2.10 方法重写与重载
方法重写(Override)
子类重新定义从父类继承的方法,提供自己的实现。
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override // 注解标记,编译器会检查是否正确重写
public void makeSound() {
System.out.println("狗汪汪叫");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("猫喵喵叫");
}
}重写规则(五同两小一大)
| 规则 | 说明 | 示例 |
|---|---|---|
| 方法名相同 | 子类方法名必须与父类完全一致 | makeSound() = makeSound() |
| 参数相同 | 参数列表(类型、数量、顺序)必须完全一致 | void eat(String food) = void eat(String food) |
| 返回类型相同或子类型 | Java 5+ 支持协变返回类型 | 父类返回 Animal,子类可返回 Dog(Dog extends Animal) |
| 访问修饰符不能更严格 | 只能扩大,不能缩小 | 父类 protected → 子类 public(OK);父类 public → 子类 protected(NO) |
| 不能抛出更宽泛异常 | 只能缩小或相同 | 父类抛 Exception → 子类可抛 IOException(更具体) |
// 协变返回类型示例
class Animal {
public Animal reproduce() { return new Animal(); }
}
class Dog extends Animal {
@Override
public Dog reproduce() { return new Dog(); } // 返回子类型,OK
}不能被重写的情况
| 类型 | 原因 |
|---|---|
final 方法 | 被 final 锁定,不可改变 |
static 方法 | 属于类,不能重写(只能隐藏) |
private 方法 | 子类不可见,谈不上重写 |
| 构造方法 | 不在继承范围内 |
方法重载(Overload)
在同一个类中,定义多个同名但参数列表不同的方法。
public class Calculator {
// 重载:参数数量不同
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载:参数类型不同
public double add(double a, double b) {
return a + b;
}
// 重载:参数顺序不同(类型不同)
public String add(String prefix, int value) {
return prefix + value;
}
public String add(int value, String suffix) {
return value + suffix;
}
// 错误示例:只有返回类型不同,不是重载
// public long add(int a, int b) { return a + b; } // 编译错误!
}重载的选择规则
编译器根据调用时传入的实参类型和数量来决定调用哪个重载方法:
public class OverloadResolution {
public void test(int x) {
System.out.println("int 版本:" + x);
}
public void test(double x) {
System.out.println("double 版本:" + x);
}
public void test(String x) {
System.out.println("String 版本:" + x);
}
public static void main(String[] args) {
OverloadResolution demo = new OverloadResolution();
demo.test(10); // int 版本
demo.test(10.5); // double 版本
demo.test("hello"); // String 版本
// 自动类型提升
demo.test('A'); // char → int,调用 int 版本
}
}重写(Override) vs 重载(Overload) 对比
| 对比维度 | 重写 (Override) | 重载 (Overload) |
|---|---|---|
| 发生位置 | 父子类之间 | 同一个类中 |
| 方法签名 | 方法名 + 参数列表完全一致 | 方法名相同,参数列表不同 |
| 返回类型 | 相同或协变子类型 | 可以不同 |
| 访问修饰符 | 不能比父类更严格 | 可以任意 |
| 异常 | 不能比父类更宽泛 | 可以任意 |
| 多态类型 | 运行时多态(动态绑定) | 编译时多态(静态绑定) |
| 注解 | 使用 @Override 标注 | 无需特殊注解 |
| static 方法 | 不能重写(只能隐藏) | 可以重载 |
| final 方法 | 不能重写 | 可以重载 |
| 构造方法 | 不能重写 | 可以重载 |
2.11 多态
核心概念
多态(Polymorphism)是指同一个行为具有多个不同的表现形式。在 Java 中,多态主要通过父类引用指向子类对象来实现。
多态实现的三个必要条件
1. 继承关系(extends)
2. 方法重写(@Override)
3. 向上转型(父类引用指向子类对象)基本示例
// 1. 定义父类
abstract class Shape {
public abstract double getArea();
public void describe() {
System.out.println("这是一个图形,面积为:" + getArea());
}
}
// 2. 子类重写方法
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
// 3. 向上转型 + 多态调用
public class PolymorphismDemo {
public static void main(String[] args) {
// 向上转型:父类引用指向子类对象
Shape s1 = new Circle(5.0);
Shape s2 = new Rectangle(4.0, 6.0);
// 多态调用:运行时根据实际对象类型调用对应方法
s1.describe(); // 输出:这是一个图形,面积为:78.5398...
s2.describe(); // 输出:这是一个图形,面积为:24.0
// 使用数组或集合统一处理
Shape[] shapes = {
new Circle(3.0),
new Rectangle(2.0, 5.0),
new Circle(4.0)
};
double totalArea = 0;
for (Shape s : shapes) {
totalArea += s.getArea(); // 多态调用
}
System.out.println("总面积:" + totalArea);
}
}编译时多态 vs 运行时多态
| 类型 | 别称 | 实现机制 | 决定时机 | 典型场景 |
|---|---|---|---|---|
| 编译时多态 | 静态多态 | 方法重载(Overload) | 编译期确定 | 同名不同参的方法 |
| 运行时多态 | 动态多态 | 方法重写(Override)+ 动态绑定 | 运行期确定 | 父类引用调用子类重写方法 |
向上转型(Upcasting)
向上转型是自动的、安全的。父类引用只能调用父类中定义的方法,不能调用子类特有的方法。
class Animal {
public void eat() { System.out.println("吃东西"); }
}
class Dog extends Animal {
public void eat() { System.out.println("吃骨头"); }
public void bark() { System.out.println("汪汪叫"); }
}
public class UpcastingDemo {
public static void main(String[] args) {
Animal a = new Dog(); // 向上转型(自动)
a.eat(); // 输出:吃骨头(多态,调用 Dog 的 eat)
// a.bark(); // 编译错误!Animal 引用看不到 Dog 特有方法
}
}向下转型(Downcasting)
向下转型需要显式强制转换,且必须先用 instanceof 检查类型,否则可能导致 ClassCastException。
public class DowncastingDemo {
public static void main(String[] args) {
Animal a = new Dog(); // 实际是 Dog 对象
// 向下转型:必须显式转换
if (a instanceof Dog) {
Dog d = (Dog) a; // 安全的向下转型
d.bark(); // 输出:汪汪叫
}
// 错误示例:不安全的向下转型
Animal a2 = new Animal();
// Dog d2 = (Dog) a2; // 运行时抛出 ClassCastException!
// 安全做法:始终先检查
if (a2 instanceof Dog) {
Dog d2 = (Dog) a2;
d2.bark();
} else {
System.out.println("a2 不是 Dog 类型");
}
}
}instanceof 关键字
instanceof 用于判断一个对象是否是指定类型(或其子类型)的实例。
public class InstanceofDemo {
public static void main(String[] args) {
Object obj1 = "Hello World";
Object obj2 = 42;
Object obj3 = new ArrayList<>();
Object obj4 = null;
System.out.println(obj1 instanceof String); // true
System.out.println(obj1 instanceof Object); // true(String 是 Object 的子类)
System.out.println(obj2 instanceof Integer); // true
System.out.println(obj3 instanceof List); // true(ArrayList 实现了 List)
System.out.println(obj4 instanceof String); // false(null 不是任何类型的实例)
// 模式匹配(Java 16+)
if (obj1 instanceof String s && s.length() > 0) {
System.out.println("字符串长度为:" + s.length()); // 直接在条件中使用 s
}
}
}多态的注意事项
| 注意点 | 说明 |
|---|---|
| 静态方法不支持多态 | 静态方法调用基于引用类型,不是实际对象类型 |
| 私有方法不能被重写 | 因此不存在多态 |
| 构造方法不支持多态 | 构造方法不是通过常规方法调用机制执行的 |
| 成员变量没有多态 | 成员变量访问看引用类型,方法调用看实际对象类型 |
class Parent {
public String name = "Parent";
public static void staticMethod() { System.out.println("Parent static"); }
public void instanceMethod() { System.out.println("Parent instance"); }
}
class Child extends Parent {
public String name = "Child"; // 隐藏父类变量
public static void staticMethod() { System.out.println("Child static"); }
@Override
public void instanceMethod() { System.out.println("Child instance"); }
}
public class PolymorphismTrap {
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.name); // 输出:Parent(变量看引用类型)
p.staticMethod(); // 输出:Parent static(静态方法看引用类型)
p.instanceMethod(); // 输出:Child instance(实例方法看实际类型)
}
}2.12 封装
核心概念
封装(Encapsulation)是将数据(属性)和操作数据的方法绑定在一起,隐藏内部实现细节,只暴露必要的接口给外部使用。
封装的基本原则
public class BankAccount {
// 1. 所有字段设为 private
private String accountNumber;
private String ownerName;
private double balance;
private String password;
// 2. 提供公共的 getter/setter 方法
public String getAccountNumber() {
return accountNumber;
}
public String getOwnerName() {
return ownerName;
}
public void setOwnerName(String ownerName) {
if (ownerName == null || ownerName.trim().isEmpty()) {
throw new IllegalArgumentException("所有者姓名不能为空");
}
this.ownerName = ownerName;
}
public double getBalance() {
return balance;
}
// 3. 不直接暴露 setter,而是提供业务方法
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("存款金额必须大于0");
}
this.balance += amount;
logTransaction("存款", amount);
}
public void withdraw(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("取款金额必须大于0");
}
if (amount > this.balance) {
throw new IllegalStateException("余额不足");
}
this.balance -= amount;
logTransaction("取款", amount);
}
// 4. 内部辅助方法设为 private
private void logTransaction(String type, double amount) {
System.out.println("[交易记录] " + type + ": " + amount + " | 余额: " + balance);
}
}
// 外部使用
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// account.balance = 100000; // 编译错误:balance 是 private
// account.password = "123"; // 编译错误:password 是 private
account.deposit(1000); // 通过公共方法操作
account.withdraw(300);
System.out.println("余额:" + account.getBalance()); // 700.0
}
}封装的四个层次
访问控制逐级放松:
┌────────────────────────────────────────────┐
│ private 同类内 │
│ ┌──────────────────────────────────────┐ │
│ │ default 同包内 │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ protected 同包 + 子类 │ │ │
│ │ │ ┌──────────────────────────┐ │ │ │
│ │ │ │ public 所有类均可访问 │ │ │ │
│ │ │ └──────────────────────────┘ │ │ │
│ │ └────────────────────────────────┘ │ │
│ └──────────────────────────────────────┘ │
└────────────────────────────────────────────┘封装的好处
| 好处 | 说明 | 示例 |
|---|---|---|
| 隐藏实现细节 | 外部只需知道"做什么",无需知道"怎么做" | 调用 deposit() 不知内部如何记账 |
| 数据验证 | 在 setter 或业务方法中统一校验 | 防止余额出现负数 |
| 灵活修改 | 内部实现改变不影响外部调用者 | 改用数据库存储,外部调用代码不变 |
| 提高安全性 | 防止外部直接篡改内部数据 | 密码字段不暴露 getter |
| 降低耦合 | 模块间通过接口通信 | 便于单元测试和维护 |
封装最佳实践
public class Person {
// 所有字段 private
private String name;
private int age;
private String email;
// 构造方法中进行参数验证
public Person(String name, int age, String email) {
setName(name); // 复用验证逻辑
setAge(age);
setEmail(email);
}
// getter:只读属性
public String getName() { return name; }
public int getAge() { return age; }
// setter:包含验证逻辑
public void setName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("姓名不能为空");
}
this.name = name;
}
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年龄必须在 0-150 之间");
}
this.age = age;
}
public void setEmail(String email) {
if (email != null && !email.contains("@")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
this.email = email;
}
// 需要时才暴露 getter(最小化暴露面)
// email 不提供 getter,只有需要发送邮件时才内部使用
}2.13 抽象类与接口
抽象类(Abstract Class)
使用 abstract 关键字修饰的类。抽象类不能实例化,通常作为其他类的基类。
// 抽象类
public abstract class Animal {
// 成员变量
protected String name;
// 构造方法(抽象类可以有构造方法)
public Animal(String name) {
this.name = name;
}
// 抽象方法:没有方法体,子类必须实现
public abstract void makeSound();
// 具体方法:有方法体,可以被子类继承和使用
public void eat() {
System.out.println(name + " 正在吃东西");
}
public void sleep() {
System.out.println(name + " 正在睡觉");
}
}
// 子类实现抽象类
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
// 必须重写(实现)所有抽象方法
@Override
public void makeSound() {
System.out.println(name + " 汪汪叫!");
}
}
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " 喵喵叫!");
}
}
public class Main {
public static void main(String[] args) {
// Animal a = new Animal("xx"); // 编译错误!抽象类不能实例化
Animal dog = new Dog("旺财");
Animal cat = new Cat("咪咪");
dog.makeSound(); // 旺财 汪汪叫!
dog.eat(); // 旺财 正在吃东西
cat.makeSound(); // 咪咪 喵喵叫!
cat.eat(); // 咪咪 正在吃东西
}
}抽象类特点
| 特点 | 说明 |
|---|---|
abstract 修饰 | 必须使用 abstract 关键字声明 |
| 不能实例化 | new Animal() 编译错误 |
| 可有构造方法 | 供子类通过 super() 调用 |
| 可有抽象方法 | 没有方法体,abstract 修饰,子类必须实现 |
| 可有具体方法 | 有方法体的普通方法 |
| 可有成员变量 | 可以有实例变量和各种修饰符的变量 |
| 单继承 | 一个类只能继承一个抽象类 |
接口(Interface)
接口是一种更纯粹的抽象类型,JDK 8+ 支持 default 和 static 方法。
// 定义接口
public interface Flyable {
// 常量:默认 public static final
int MAX_SPEED = 1000;
String TYPE = "飞行器";
// 抽象方法:默认 public abstract
void fly();
void land();
// Java 8+:default 方法(有默认实现)
default void takeOff() {
System.out.println("准备起飞...");
checkEngine();
}
// Java 8+:static 方法
static String getFlyableType() {
return "这是一个可飞行类型";
}
// Java 9+:private 方法(供 default 方法内部复用)
private void checkEngine() {
System.out.println("检查引擎状态...");
}
}
// 另一个接口
public interface Swimmable {
void swim();
default void dive() {
System.out.println("潜入水中...");
}
}
// 一个类可以实现多个接口
public class Duck implements Flyable, Swimmable {
private String name;
public Duck(String name) {
this.name = name;
}
@Override
public void fly() {
System.out.println(name + " 在飞行");
}
@Override
public void land() {
System.out.println(name + " 降落在水面");
}
@Override
public void swim() {
System.out.println(name + " 在游泳");
}
}
// 接口使用
public class Main {
public static void main(String[] args) {
Duck duck = new Duck("唐老鸭");
duck.takeOff(); // 调用接口 default 方法
duck.fly();
duck.land();
duck.swim();
duck.dive(); // 调用接口 default 方法
// 访问接口常量
System.out.println("最大速度:" + Flyable.MAX_SPEED);
// 调用接口 static 方法
System.out.println(Flyable.getFlyableType());
}
}接口特点
| 特点 | 说明 |
|---|---|
interface 声明 | 使用 interface 关键字,非 class |
| 不能实例化 | new Flyable() 编译错误 |
| 无构造方法 | 接口没有构造方法 |
所有变量默认 public static final | 必须是常量 |
方法默认 public abstract | JDK 8 前所有方法都是抽象方法 |
| JDK 8+ default 方法 | 有方法体的默认方法(可被重写) |
| JDK 8+ static 方法 | 接口的静态工具方法 |
| JDK 9+ private 方法 | 接口内部的私有辅助方法 |
| 多实现 | 一个类可以实现多个接口 |
接口的多实现和冲突解决
interface A {
default void hello() {
System.out.println("Hello from A");
}
}
interface B {
default void hello() {
System.out.println("Hello from B");
}
}
// 当多个接口有同名 default 方法时,必须重写解决冲突
class C implements A, B {
@Override
public void hello() {
// 选择调用某一个接口的实现
A.super.hello(); // 调用 A 的 hello
B.super.hello(); // 调用 B 的 hello
System.out.println("Hello from C");
}
}抽象类 vs 接口 对比
| 对比维度 | 抽象类 (abstract class) | 接口 (interface) |
|---|---|---|
| 关键字 | abstract class | interface |
| 继承/实现 | 单继承(extends) | 多实现(implements) |
| 构造方法 | 有构造方法 | 没有构造方法 |
| 实例变量 | 可以有各种变量 | 只有 public static final 常量 |
| 方法 | 可有抽象方法和具体方法 | JDK 8 前只有抽象方法;JDK 8+ 支持 default/static |
| 访问修饰符 | 方法可以有任意访问级别 | 方法默认 public |
| 成员访问 | 可以有任何访问修饰符 | 所有成员默认 public |
| 设计关系 | "is-a" 关系,表示本质 | "can-do" 关系,表示能力 |
| 使用场景 | 共享代码 + 模板方法模式 | 定义契约(行为规范) |
如何选择
使用抽象类的情况:
- 需要在子类间共享代码(有方法实现)
- 需要定义非 public 的成员
- 需要使用构造方法初始化状态
- 符合严格的 is-a 关系
使用接口的情况:
- 定义不相关的类之间的共同行为(can-do)
- 需要多重继承行为
- 只定义契约,不关心实现细节
- 希望支持未来的扩展(default 方法可向后兼容添加)
组合使用的情况:
- 抽象类实现接口,提供默认实现
- 如:public abstract class AbstractList<E> implements List<E> // 组合使用示例
interface Payable {
void pay(double amount);
default void refund(double amount) {
System.out.println("退款:" + amount);
}
}
abstract class AbstractPayment implements Payable {
protected String merchantId;
public AbstractPayment(String merchantId) {
this.merchantId = merchantId;
}
// 提供通用的验证逻辑
protected void validateAmount(double amount) {
if (amount <= 0) throw new IllegalArgumentException("金额无效");
}
// 留给子类实现
@Override
public abstract void pay(double amount);
}
class WechatPay extends AbstractPayment {
public WechatPay(String merchantId) {
super(merchantId);
}
@Override
public void pay(double amount) {
validateAmount(amount);
System.out.println("微信支付:" + amount);
}
}2.14 内部类
概述
内部类是定义在另一个类内部的类。Java 支持四种内部类:成员内部类、静态嵌套类、局部内部类、匿名类。
1. 成员内部类(非静态内部类)
定义在类中方法外的非静态类。
public class Outer {
private String outerField = "外部类字段";
private static String staticField = "静态字段";
// 成员内部类
public class Inner {
// 不能定义静态成员(除 static final 常量外)
// private static int x; // 编译错误!
public void display() {
// 可以访问外部类的所有成员(包括 private)
System.out.println("访问外部实例字段:" + outerField);
System.out.println("访问外部静态字段:" + staticField);
// 持有外部类引用
System.out.println("外部类引用:" + Outer.this);
}
}
public void createInner() {
Inner inner = new Inner(); // 在外部类内部创建
inner.display();
}
}
// 外部创建
public class Main {
public static void main(String[] args) {
// 创建成员内部类需要先有外部类对象
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.display();
}
}2. 静态嵌套类(Static Nested Class)
使用 static 修饰的内部类。
public class Outer {
private String instanceField = "实例字段";
private static String staticField = "静态字段";
// 静态嵌套类
public static class StaticNested {
private String nestedField = "嵌套类字段";
public void display() {
// 只能访问外部类的静态成员
System.out.println("访问静态字段:" + staticField);
// System.out.println(instanceField); // 编译错误!不能访问实例成员
// 不持有外部类引用,可以独立存在
}
public static void staticMethod() {
System.out.println("静态嵌套类的静态方法");
}
}
}
// 使用
public class Main {
public static void main(String[] args) {
// 不需要外部类对象,直接创建
Outer.StaticNested nested = new Outer.StaticNested();
nested.display();
// 调用静态方法
Outer.StaticNested.staticMethod();
// 常见应用:Builder 模式
Computer pc = new Computer.Builder("i7", "16GB").gpu("RTX 4080").build();
}
}3. 局部内部类
定义在方法、构造方法或代码块中的类。
public class Outer {
public void processData(String prefix) {
int localVar = 100; // 事实 final
// 局部内部类
class DataProcessor {
private String name;
public DataProcessor(String name) {
this.name = name;
}
public void process() {
// 只能访问 final 或事实 final 的局部变量
System.out.println(prefix + name + " " + localVar);
// localVar = 200; // 如果修改 localVar,上一行编译错误!
// 因为局部变量被内部类捕获后必须不可变
}
}
DataProcessor processor = new DataProcessor("处理器");
processor.process();
}
}4. 匿名类
没有名字的内部类,用于实现接口或继承类的一次性使用。
public class AnonymousClassDemo {
public static void main(String[] args) {
// 1. 匿名类实现接口
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("匿名类实现的线程任务");
}
};
new Thread(task).start();
// 2. 匿名类继承抽象类
Animal dog = new Animal("旺财") {
@Override
public void makeSound() {
System.out.println(name + " 汪汪叫!(来自匿名类)");
}
};
dog.makeSound();
// 3. 匿名类作为参数传递
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// 4. JDK 8+ 可用 Lambda 替代匿名类
Collections.sort(names, (a, b) -> a.length() - b.length());
}
}四种内部类对比
| 类型 | 修饰符 | 访问外部成员 | 持有外部引用 | 可定义静态成员 | 典型用途 |
|---|---|---|---|---|---|
| 成员内部类 | 无 static | 可访问所有 | 是 | 否(除 static final 常量) | 与外部类紧密关联的辅助类 |
| 静态嵌套类 | static | 仅静态成员 | 否 | 是 | Builder 模式、独立辅助类 |
| 局部内部类 | 不能用访问修饰符 | 可访问所有 + final 局部变量 | 是 | 否 | 方法内临时使用的类 |
| 匿名类 | 无名 | 可访问所有 + final 局部变量 | 是 | 否 | 简单的一次性实现(监听器、Comparator) |
选择指南
需要访问外部类实例成员 → 成员内部类
不需要外部类实例 → 静态嵌套类(优先选择,内存效率更高)
简单接口的一次性实现 → 匿名类(或 Lambda)
方法内部临时使用 → 局部内部类内存效率排序
静态嵌套类 > 局部内部类 > 成员内部类 > 匿名类原因:静态嵌套类不持有外部引用,没有额外的内存开销;匿名类和成员内部类都隐式持有外部类引用,可能阻止外部类被 GC 回收。
2.15 枚举(Enum)
核心概念
枚举(Enum)是一种特殊的类,用于定义一组固定的常量。所有枚举类都隐式继承 java.lang.Enum。
基本定义与使用
// 简单枚举
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// 使用
public class EnumBasicDemo {
public static void main(String[] args) {
Day today = Day.FRIDAY;
// switch 中使用枚举
switch (today) {
case MONDAY:
System.out.println("周一综合征");
break;
case FRIDAY:
System.out.println("TGIF!");
break;
case SATURDAY:
case SUNDAY:
System.out.println("周末愉快!");
break;
default:
System.out.println("工作日");
}
}
}带属性和方法的枚举
public enum Season {
// 枚举实例(调用构造方法)
SPRING("春天", "温暖", 1),
SUMMER("夏天", "炎热", 2),
AUTUMN("秋天", "凉爽", 3),
WINTER("冬天", "寒冷", 4);
// 成员变量
private final String chineseName;
private final String description;
private final int order;
// 构造方法必须是 private(默认就是 private)
Season(String chineseName, String description, int order) {
this.chineseName = chineseName;
this.description = description;
this.order = order;
}
// getter 方法
public String getChineseName() { return chineseName; }
public String getDescription() { return description; }
public int getOrder() { return order; }
// 自定义方法
public boolean isHotSeason() {
return this == SUMMER;
}
public boolean isColdSeason() {
return this == WINTER;
}
}
// 使用
public class Main {
public static void main(String[] args) {
Season s = Season.SUMMER;
System.out.println(s.getChineseName()); // 夏天
System.out.println(s.getDescription()); // 炎热
System.out.println(s.isHotSeason()); // true
}
}枚举的常用方法
public class EnumMethodsDemo {
public static void main(String[] args) {
Day day = Day.FRIDAY;
// name():返回枚举常量名(字符串)
System.out.println(day.name()); // FRIDAY
// ordinal():返回枚举常量的序数(从 0 开始)
System.out.println(day.ordinal()); // 4(第 5 个枚举值)
// toString():默认返回 name(),可重写
System.out.println(day.toString()); // FRIDAY
// valueOf(String):通过名称获取枚举常量
Day d = Day.valueOf("MONDAY");
System.out.println(d); // MONDAY
// Day d2 = Day.valueOf("INVALID"); // 抛出 IllegalArgumentException
// values():返回所有枚举常量的数组
Day[] allDays = Day.values();
for (Day each : allDays) {
System.out.print(each + " ");
}
// 输出:MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY
// compareTo():比较顺序(基于 ordinal)
System.out.println(Day.MONDAY.compareTo(Day.FRIDAY)); // 负数(MONDAY 在 FRIDAY 之前)
System.out.println(Day.FRIDAY.compareTo(Day.MONDAY)); // 正数
System.out.println(Day.MONDAY.compareTo(Day.MONDAY)); // 0
}
}枚举比较
public class EnumComparison {
public static void main(String[] args) {
Day d1 = Day.MONDAY;
Day d2 = Day.MONDAY;
Day d3 = Day.FRIDAY;
// == 和 equals() 都可以用于枚举比较(推荐使用 ==,因为枚举是单例)
System.out.println(d1 == d2); // true
System.out.println(d1.equals(d2)); // true
System.out.println(d1 == d3); // false
// 枚举实现了 Comparable 接口
// 不能用 > 和 < 直接比较!
// if (d1 < d3) // 编译错误!
// 使用 compareTo() 进行比较
if (d1.compareTo(d3) < 0) {
System.out.println(d1 + " 在 " + d3 + " 之前");
}
}
}枚举实现接口
// 定义接口
interface Operator {
double apply(double a, double b);
}
// 枚举实现接口
public enum MathOperator implements Operator {
ADD {
@Override
public double apply(double a, double b) {
return a + b;
}
},
SUBTRACT {
@Override
public double apply(double a, double b) {
return a - b;
}
},
MULTIPLY {
@Override
public double apply(double a, double b) {
return a * b;
}
},
DIVIDE {
@Override
public double apply(double a, double b) {
if (b == 0) throw new ArithmeticException("除数不能为0");
return a / b;
}
};
}
// 使用
public class Main {
public static void main(String[] args) {
double result = MathOperator.ADD.apply(10, 5);
System.out.println("10 + 5 = " + result); // 15.0
double result2 = MathOperator.MULTIPLY.apply(3, 4);
System.out.println("3 * 4 = " + result2); // 12.0
}
}枚举的注意事项
| 要点 | 说明 |
|---|---|
| 构造方法必须是 private | 枚举构造方法默认就是 private,且不能改为 public 或 protected |
| 不能用 > 和 < 直接比较 | 枚举不支持 < > 运算符,使用 compareTo() 方法 |
| == 和 equals() 都可用 | 枚举是单例模式,推荐使用 == 进行引用比较(更高效,且不会 NPE) |
ordinal() 不建议在业务逻辑中使用 | ordinal() 依赖枚举声明顺序,调整顺序会导致逻辑错误。应使用自定义属性替代 |
| 可以实现接口 | 枚举可以实现一个或多个接口 |
| 不能继承类 | 枚举已经隐式继承 java.lang.Enum,Java 单继承限制导致不能再继承其他类 |
// 正确实践:用自定义属性替代 ordinal()
public enum ErrorCode {
SUCCESS(200, "成功"),
BAD_REQUEST(400, "错误的请求"),
UNAUTHORIZED(401, "未授权"),
NOT_FOUND(404, "未找到"),
INTERNAL_ERROR(500, "服务器内部错误");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() { return code; }
public String getMessage() { return message; }
// 通过 code 查找对应的枚举值
public static ErrorCode fromCode(int code) {
for (ErrorCode ec : values()) {
if (ec.code == code) {
return ec;
}
}
throw new IllegalArgumentException("未知的错误码:" + code);
}
}2.16 递归
核心概念
递归(Recursion)是指一个方法在其方法体内调用自身来解决问题的编程技巧。递归通常将一个复杂问题分解为规模更小的同类子问题。
递归的三要素
1. 递归终止条件(Base Case):什么情况下停止递归
2. 递归调用(Recursive Call):如何将问题分解为更小的子问题
3. 递归前进(Progress):每次调用都要向终止条件靠近线性递归
每次递归只产生一个子问题,递归深度与问题规模成正比 O(n)。
public class LinearRecursion {
/**
* 计算阶乘 n!
* 递归公式:n! = n × (n−1)!,且 0! = 1
*/
public static long factorial(int n) {
// 终止条件(Base Case)
if (n == 0 || n == 1) {
return 1;
}
// 递归调用:n * (n-1)!
return n * factorial(n - 1);
}
/**
* 计算斐波那契数列第 n 项(线性递归优化版)
* F(0) = 0, F(1) = 1, F(n) = F(n-1) + F(n-2)
*/
public static long fibonacci(int n) {
return fibonacciHelper(n, 0, 1);
}
// 尾递归辅助方法(Java 不优化尾递归,但逻辑清晰)
private static long fibonacciHelper(int n, long a, long b) {
if (n == 0) return a;
if (n == 1) return b;
return fibonacciHelper(n - 1, b, a + b);
}
/**
* 计算数组元素之和
*/
public static int arraySum(int[] arr, int index) {
if (index >= arr.length) {
return 0; // 终止条件
}
return arr[index] + arraySum(arr, index + 1);
}
/**
* 反转字符串
*/
public static String reverse(String str) {
if (str.isEmpty()) {
return str; // 终止条件
}
return reverse(str.substring(1)) + str.charAt(0);
}
public static void main(String[] args) {
System.out.println("5! = " + factorial(5)); // 120
System.out.println("F(10) = " + fibonacci(10)); // 55
System.out.println("数组和 = " + arraySum(new int[]{1,2,3,4,5}, 0)); // 15
System.out.println("反转 = " + reverse("Hello")); // olleH
}
}二分递归
每次递归产生两个子问题,可能导致大量重复计算。时间复杂度通常为 O(2^n)。
public class BinaryRecursion {
/**
* 斐波那契数列(朴素二分递归)
* 问题:存在大量重复计算。F(5) 需要计算 F(3) 两次,F(2) 三次...
*/
public static long fibonacciNaive(int n) {
// 终止条件
if (n <= 1) {
return n;
}
// 二分递归:每次产生两个子问题
return fibonacciNaive(n - 1) + fibonacciNaive(n - 2);
}
/**
* 二分查找(递归实现)
*/
public static int binarySearch(int[] arr, int target, int left, int right) {
if (left > right) {
return -1; // 未找到
}
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] > target) {
return binarySearch(arr, target, left, mid - 1);
} else {
return binarySearch(arr, target, mid + 1, right);
}
}
/**
* 归并排序(递归实现)
*/
public static void mergeSort(int[] arr, int left, int right) {
if (left >= right) {
return; // 终止条件:单个元素或空区间
}
int mid = left + (right - left) / 2;
// 二分递归
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并
merge(arr, left, mid, right);
}
private static void merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[right - left + 1];
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right) {
temp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
}
while (i <= mid) { temp[k++] = arr[i++]; }
while (j <= right) { temp[k++] = arr[j++]; }
System.arraycopy(temp, 0, arr, left, temp.length);
}
public static void main(String[] args) {
// 斐波那契(注意:n=50 时朴素版本会很慢)
System.out.println("F(10) 朴素 = " + fibonacciNaive(10)); // 55
// 二分查找
int[] arr = {1, 3, 5, 7, 9, 11, 13};
int index = binarySearch(arr, 7, 0, arr.length - 1);
System.out.println("7 的索引 = " + index); // 3
// 归并排序
int[] data = {38, 27, 43, 3, 9, 82, 10};
mergeSort(data, 0, data.length - 1);
System.out.println("排序结果:" + Arrays.toString(data));
// [3, 9, 10, 27, 38, 43, 82]
}
}线性递归 vs 二分递归对比
| 对比维度 | 线性递归 | 二分递归 |
|---|---|---|
| 子问题数量 | 每次 1 个 | 每次 2 个 |
| 时间复杂度 | 通常 O(n) | 通常 O(2^n)(朴素)、O(n log n)(归并/快排) |
| 重复计算 | 一般不重复 | 可能大量重复(如朴素斐波那契) |
| 递归深度 | 与输入规模成正比 | 通常 log n 级别(分治) |
| 典型应用 | 阶乘、链表遍历、字符串处理 | 斐波那契、归并排序、快速排序、二叉树遍历 |
| 优化策略 | 可转为尾递归/迭代 | 可使用记忆化搜索(Memoization) |
递归优化:记忆化搜索
public class MemoizationDemo {
// 记忆化搜索:缓存已计算的结果,避免重复计算
private static Map<Integer, Long> memo = new HashMap<>();
public static long fibonacciMemo(int n) {
if (n <= 1) {
return n;
}
// 如果已经计算过,直接返回缓存结果
if (memo.containsKey(n)) {
return memo.get(n);
}
// 计算并缓存
long result = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
memo.put(n, result);
return result;
}
// 数组版本(更高效)
public static long fibonacciMemoArray(int n) {
long[] memo = new long[n + 1];
return fib(n, memo);
}
private static long fib(int n, long[] memo) {
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
return memo[n];
}
public static void main(String[] args) {
System.out.println("F(50) 记忆化 = " + fibonacciMemo(50));
// 快速计算结果,而不是像朴素版本那样指数级耗时
}
}递归的注意事项
| 注意点 | 说明 |
|---|---|
| 必须有终止条件 | 否则会导致无限递归,最终触发 StackOverflowError |
| 向终止条件靠近 | 每次递归调用都应在问题规模上向 Base Case 靠近 |
| 注意栈溢出 | Java 栈深度有限(默认约 1MB),深度递归可能 StackOverflowError |
| 警惕重复计算 | 二分递归可能指数级重复,使用记忆化或动态规划优化 |
| 递归转迭代 | 任何递归都可以转换为迭代实现,迭代更省内存 |
| 避免过深递归 | 对于超大数据量,优先考虑迭代方案 |
// 递归转迭代示例:阶乘
public static long factorialIterative(int n) {
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// 递归转迭代示例:斐波那契(动态规划)
public static long fibonacciDP(int n) {
if (n <= 1) return n;
long prev2 = 0, prev1 = 1, current = 0;
for (int i = 2; i <= n; i++) {
current = prev2 + prev1;
prev2 = prev1;
prev1 = current;
}
return current;
}全文完。本笔记涵盖了 Java 面向对象编程的核心知识点,包括类与对象、方法、构造方法、访问修饰符、this/super/static/final 关键字、继承、封装、多态、抽象类与接口、内部类、枚举和递归等 16 个主题,适合作为 Java OOP 学习与复习的参考资料。
评论