面向对象编程(OOP):Java 的核心思想(详细笔记)
3.1 OOP思想概述:从“过程”到“对象”的转变
面向对象编程(Object-Oriented Programming,OOP)是Java的核心设计思想,它将现实世界中的“事物”抽象为“对象”,通过“类”定义对象的共性,以“封装、继承、多态”三大特性实现代码的可复用、可维护、可扩展。
3.1.1 什么是面向对象?
-
核心逻辑:将问题拆解为“对象”,通过对象之间的“交互”完成功能,而非像“面向过程”那样按步骤编写指令。 **对象(Object)**是程序的基本单元,它是对现实世界中具体实体的抽象和模拟。简单来说,对象就像现实世界中的一个 “东西”,拥有自己的特征(属性、状态)和能做的事情(行为、方法);换言之,对象是一种特殊的数据结构,可以用来记住一个事物的数据,从而代表该事物。
例:实现“学生管理系统”- 面向过程:编写“添加学生步骤”“查询学生步骤”“删除学生步骤”等线性代码;
- 面向对象:抽象出
Student
(学生对象,含姓名、学号等属性)、StudentManager
(管理对象,含添加、查询等方法),通过两个对象的交互实现功能。
-
OOP的优势:
- 代码复用:通过继承复用父类代码,避免重复编写;
- 隐藏细节:通过封装隐藏对象内部实现,仅暴露安全的访问接口;
- 灵活扩展:通过多态支持“同一行为不同实现”,新增功能无需修改原有代码(符合“开闭原则”);
- 便于维护:对象与现实事物映射,代码结构清晰,问题定位更简单。
3.1.2 OOP三大核心特性
OOP的核心是“封装、继承、多态”,三者相互支撑,构成面向对象的基础:
特性 | 核心作用 | 实现方式 |
---|---|---|
封装(Encapsulation) | 隐藏对象内部细节,保护数据安全 | 访问修饰符(private等)+ getter/setter |
继承(Inheritance) | 复用父类代码,建立类的层级关系 | extends关键字 |
多态(Polymorphism) | 同一行为的不同表现形式,提升灵活性 | 继承+方法重写+向上转型 |
3.2 类与对象:OOP的“基本单元”
类是“对象的模板”,定义对象的属性(数据)和方法(行为);对象是“类的实例”,是具体的、可操作的实体。二者是OOP的基础,必须先定义类,才能创建对象。
3.2.1 类的定义:模板的“蓝图”
Java中用class
关键字定义类,类的组成包括:
- 成员变量:描述对象的属性(如学生的姓名、学号);
- 成员方法:描述对象的行为(如学生的学习、考试);
- 构造方法:用于创建对象时初始化数据(与类名相同,无返回值);
- 代码块:初始化类或对象(静态代码块、实例代码块,暂略)。
类的定义语法与实例(以Student
类为例):
// 定义Student类(大驼峰命名,符合标识符规范)
public class Student {// 1. 成员变量(属性):描述学生的特征,通常用private修饰(封装)private String name; // 姓名private int id; // 学号private int age; // 年龄private double score; // 成绩// 2. 构造方法:用于创建对象,初始化成员变量// 2.1 无参构造方法(默认存在,若定义了带参构造,需手动显式定义)public Student() {// 可默认初始化,如this.age = 18;}// 2.2 带参构造方法(重载,根据需求定义多个)public Student(String name, int id, int age) {this.name = name; // this:指代当前对象,区分成员变量与局部变量this.id = id;this.age = age;this.score = 0.0; // 成绩默认初始化为0.0}// 3. 成员方法(行为):描述学生的操作// 3.1 普通方法:学习行为public void study(String subject) {System.out.println(this.name + "正在学习" + subject);}// 3.2 有返回值的方法:考试行为,返回成绩public double takeExam() {// 模拟考试成绩(0-100随机)this.score = Math.random() * 100;System.out.println(this.name + "的考试成绩:" + Math.round(this.score));return this.score;}// 4. getter/setter方法:封装的核心,提供成员变量的安全访问接口public String getName() {return this.name; // getter:获取成员变量值}public void setName(String name) {// 可添加校验逻辑,确保数据合法(如姓名不为空)if (name != null && !name.trim().isEmpty()) {this.name = name;} else {System.out.println("姓名不能为空!");}}public int getId() {return this.id;}public void setId(int id) {if (id > 0) { // 校验学号为正数this.id = id;} else {System.out.println("学号必须为正数!");}}// age和score的getter/setter省略,写法同上
}
类定义的关键规则:
- 类名必须符合大驼峰命名法(如
Student
,而非student
或studentInfo
); - 成员变量若用
private
修饰,必须通过getter/setter
访问(封装要求); - 构造方法名必须与类名完全一致,且无返回值(连
void
都不能加); - 构造方法支持“重载”(多个构造方法参数列表不同),方便创建对象时灵活初始化。
3.2.2 对象的创建与使用:模板的“实例化”
对象是类的具体实例,通过new
关键字创建,步骤为:创建对象(new + 构造方法)→ 调用对象的属性/方法。
对象的创建与使用实例:
public class StudentTest {public static void main(String[] args) {// 1. 创建对象:new + 构造方法(调用无参构造)Student student1 = new Student();// 通过setter设置属性(因name是private,不能直接student1.name = "张三")student1.setName("张三");student1.setId(2024001);student1.setAge(20);// 调用对象的方法student1.study("Java"); // 输出:张三正在学习Javastudent1.takeExam(); // 输出:张三的考试成绩:85(随机值)// 2. 创建对象:调用带参构造(直接初始化属性,无需setter)Student student2 = new Student("李四", 2024002, 19);// 通过getter获取属性值System.out.println("学生2姓名:" + student2.getName()); // 输出:李四System.out.println("学生2学号:" + student2.getId()); // 输出:2024002student2.study("MySQL"); // 输出:李四正在学习MySQL}
}
对象的内存模型(简化):
- 堆内存:存储对象的实际数据(成员变量的值,如
student1
的name="张三"
、id=2024001
); - 栈内存:存储对象的引用(地址,如
student1
变量存储的是堆中对象的地址,而非对象本身); - 例:
Student student1 = new Student();
new Student()
:在堆中创建对象,分配内存;student1
:在栈中存储堆对象的地址,通过地址访问堆中的对象。
3.2.3 this关键字:对象的“自我引用”
this
代表“当前对象”,即调用方法/构造方法的对象,主要用途:
- 区分成员变量与局部变量:当方法参数与成员变量同名时,用
this.成员变量
指代成员变量;public void setName(String name) {this.name = name; // this.name:成员变量;name:局部变量(参数) }
- 调用当前类的其他构造方法:在构造方法中用
this(参数列表)
调用本类的其他构造方法,必须放在构造方法的第一行;public Student(String name, int id) {this(name, id, 18); // 调用本类的三参构造,年龄默认18 }public Student(String name, int id, int age) {this.name = name;this.id = id;this.age = age; }
- 调用当前类的成员方法:可省略
this
,直接调用,但加this
更清晰;public void takeExam() {this.checkAnswer(); // 调用本类的checkAnswer()方法,this可省略// ... }private void checkAnswer() {// 校验答案逻辑 }
3.3 封装:OOP的“安全屏障”
封装是指“隐藏对象的内部细节,仅通过暴露的接口与对象交互”,核心目的是保护数据不被非法修改,同时降低代码耦合度。
3.3.1 封装的实现:访问修饰符 + getter/setter
1. 访问修饰符:控制“访问权限”
Java提供4种访问修饰符,用于限制类、成员变量、成员方法的访问范围,是封装的基础:
访问修饰符 | 本类内部 | 同包其他类 | 子类(不同包) | 其他包非子类 | 常用场景 |
---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 成员变量(隐藏细节) |
default | ✅ | ✅ | ❌ | ❌ | 同包内的工具类、方法 |
protected | ✅ | ✅ | ✅ | ❌ | 子类需要继承的方法/变量 |
public | ✅ | ✅ | ✅ | ✅ | 类、对外暴露的接口(如getter/setter) |
2. getter/setter:提供“安全访问接口”
- getter方法:命名格式
public 数据类型 getXxx()
,用于获取private
修饰的成员变量值; - setter方法:命名格式
public void setXxx(数据类型 参数)
,用于设置private
修饰的成员变量值,可在方法内添加数据校验逻辑(如年龄不能为负数、姓名不能为空)。
封装实例(完善的Person
类):
public class Person {// 成员变量用private修饰,隐藏细节private String name;private int age;// 无参构造public Person() {}// 带参构造public Person(String name, int age) {this.setName(name); // 调用setter,复用校验逻辑(而非直接this.name = name)this.setAge(age);}// getter:获取姓名public String getName() {return this.name;}// setter:设置姓名,添加非空校验public void setName(String name) {if (name == null || name.trim().isEmpty()) {throw new IllegalArgumentException("姓名不能为空!"); // 抛异常,强制处理非法数据}this.name = name;}// getter:获取年龄public int getAge() {return this.age;}// setter:设置年龄,添加范围校验(0-150)public void setAge(int age) {if (age < 0 || age > 150) {throw new IllegalArgumentException("年龄必须在0-150之间!");}this.age = age;}// 对外暴露的方法(public)public void introduce() {System.out.println("大家好,我是" + this.name + ",今年" + this.age + "岁。");}
}
使用封装类的优势:
- 数据安全:非法数据(如年龄=-5)会被setter拦截,无法存入对象;
- 易于维护:若需修改校验逻辑(如年龄上限改为120),只需修改setter方法,无需修改所有调用处;
- 降低耦合:外部代码仅依赖getter/setter接口,不依赖内部实现。
3.4 继承:OOP的“代码复用”机制
继承是指“子类(Subclass)继承父类(Superclass)的属性和方法”,子类可在父类基础上新增属性/方法,或重写父类方法,实现代码复用和类的层级关系。
3.4.1 继承的实现:extends关键字
Java中用extends
关键字实现继承,语法:public class 子类名 extends 父类名 {}
。
继承的核心规则:
- 单继承:Java不支持多继承(子类只能有一个直接父类),但支持多层继承(如
Dog extends Animal
,TaiDiDog extends Dog
); - 继承内容:子类继承父类的
public
、protected
、default
(同包)成员(变量/方法),不继承private
成员(需通过父类的getter/setter访问); - 父类构造方法:子类构造方法默认调用父类的无参构造方法(若父类无无参构造,子类必须显式调用父类的带参构造,否则编译报错)。
继承实例(Animal
父类与Dog
子类):
// 父类:Animal(抽取子类的共性)
public class Animal {// 父类成员变量(protected:子类可直接访问)protected String name;protected int age;// 父类无参构造public Animal() {System.out.println("Animal无参构造被调用");}// 父类带参构造public Animal(String name, int age) {this.name = name;this.age = age;System.out.println("Animal带参构造被调用");}// 父类成员方法(子类可继承)public void eat() {System.out.println(this.name + "正在吃食物");}public void sleep() {System.out.println(this.name + "正在睡觉");}// getter/setterpublic 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;}
}// 子类:Dog(继承Animal,新增子类特性)
public class Dog extends Animal {// 子类新增成员变量(特有属性)private String breed; // 品种// 子类无参构造public Dog() {// 隐含super():调用父类无参构造,必须放在第一行(可省略)System.out.println("Dog无参构造被调用");}// 子类带参构造(显式调用父类带参构造)public Dog(String name, int age, String breed) {super(name, age); // super():调用父类带参构造,必须放在第一行this.breed = breed;System.out.println("Dog带参构造被调用");}// 子类新增方法(特有行为)public void bark() {System.out.println(this.name + "(" + this.breed + ")正在叫:汪汪汪!");}// 子类重写父类方法(Override:修改父类方法的实现)@Override // 注解:校验方法重写的合法性(如方法名、参数列表是否正确)public void eat() {// 调用父类的eat()方法(super.方法名())super.eat();// 新增子类逻辑System.out.println("它最喜欢吃骨头");}// 子类getter/setter(针对新增的breed)public String getBreed() {return breed;}public void setBreed(String breed) {this.breed = breed;}
}// 测试类
public class InheritanceTest {public static void main(String[] args) {// 创建Dog对象(调用子类带参构造)Dog dog = new Dog("旺财", 3, "中华田园犬");// 调用继承自父类的方法dog.eat(); // 输出:旺财正在吃食物 → 它最喜欢吃骨头(重写后的效果)dog.sleep(); // 输出:旺财正在睡觉(父类原方法)// 调用子类新增的方法dog.bark(); // 输出:旺财(中华田园犬)正在叫:汪汪汪!// 调用继承自父类的getterSystem.out.println(dog.getName() + "今年" + dog.getAge() + "岁"); // 输出:旺财今年3岁}
}
继承的执行结果(构造方法调用顺序):
Animal带参构造被调用 // 子类构造先调用父类构造(super(name, age))
Dog带参构造被调用 // 再执行子类构造
旺财正在吃食物 // 父类eat()方法
它最喜欢吃骨头 // 子类重写新增的逻辑
旺财正在睡觉 // 父类sleep()方法
旺财(中华田园犬)正在叫:汪汪汪!
旺财今年3岁
3.4.2 方法重写(Override):子类的“个性化改造”
方法重写是指“子类定义与父类同名、同参数列表、兼容返回值的方法”,用于修改父类方法的实现,是多态的前提。
方法重写的规则(必须满足,否则编译报错):
- 方法签名一致:方法名、参数列表(个数、类型、顺序)必须与父类完全相同;
❌ 错误:父类eat()
,子类eat(String food)
(参数列表不同,是重载而非重写); - 返回值兼容:子类方法返回值类型 <= 父类方法返回值类型(如父类返回
Animal
,子类可返回Dog
); - 访问修饰符不严格:子类方法访问修饰符 >= 父类方法访问修饰符(如父类
protected eat()
,子类可public eat()
,不能private eat()
); - 不能重写的方法:
- 父类
private
方法(子类不可见,无法重写); - 父类
final
方法(final
修饰的方法不能被重写); - 父类
static
方法(静态方法属于类,而非对象,子类若定义同名静态方法,是“隐藏”而非“重写”)。
- 父类
方法重写与方法重载的区别(易混淆点):
对比维度 | 方法重写(Override) | 方法重载(Overload) |
---|---|---|
定义位置 | 子类与父类之间 | 同一类中 |
方法签名 | 方法名、参数列表完全相同 | 方法名相同,参数列表不同(个数/类型/顺序) |
返回值 | 兼容(子类 <= 父类) | 无要求(可不同) |
访问修饰符 | 子类 >= 父类 | 无要求 |
作用 | 子类修改父类方法的实现 | 同一类中实现多态(同一行为不同参数) |
3.4.3 super关键字:子类访问父类的“桥梁”
super
代表“父类对象”,与this
类似,但this
指代当前对象,super
指代父类对象,主要用途:
- 调用父类构造方法:在子类构造方法中用
super(参数列表)
调用父类构造,必须放在子类构造的第一行;public Dog(String name, int age) {super(name, age); // 调用父类Animal的带参构造 }
- 调用父类成员方法:在子类方法中用
super.方法名()
调用父类的方法(常用于重写时复用父类逻辑);@Override public void eat() {super.eat(); // 调用父类eat()方法System.out.println("子类新增逻辑"); }
- 调用父类成员变量:在子类中用
super.成员变量名
访问父类的成员变量(当子类与父类成员变量同名时);public class Dog extends Animal {private String name; // 子类与父类成员变量同名public void showName() {System.out.println("子类name:" + this.name); // 当前对象的name(子类)System.out.println("父类name:" + super.name); // 父类对象的name} }
3.5 多态:OOP的“灵活扩展”核心
多态是指“同一行为在不同对象上有不同的表现形式”,例如“动物吃饭”,狗吃骨头、猫吃鱼、鸟吃虫,这就是多态的体现。多态的核心是“向上转型”,即父类引用指向子类对象。
3.5.1 多态的实现条件
要实现多态,必须满足三个条件:
- 存在继承关系:子类继承父类(或实现接口);
- 子类重写父类方法:子类修改父类方法的实现(多态的核心是“调用重写后的方法”);
- 向上转型:父类引用指向子类对象(
父类类型 变量名 = new 子类类型();
)。
3.5.2 多态的核心:向上转型与向下转型
1. 向上转型(自动转换,多态的前提)
- 语法:
父类类型 父类引用 = new 子类类型();
- 特点:父类引用只能调用父类中定义的方法(或子类重写的方法),不能调用子类特有的方法;
- 实例:
// 向上转型:Animal引用指向Dog对象 Animal animal = new Dog("旺财", 3, "中华田园犬");animal.eat(); // 调用子类重写的eat()方法(输出:旺财正在吃食物 → 它最喜欢吃骨头) animal.sleep(); // 调用父类的sleep()方法(输出:旺财正在睡觉) // animal.bark(); // 错误:父类Animal中无bark()方法,父类引用不能调用子类特有方法
2. 向下转型(强制转换,需谨慎)
- 语法:
子类类型 子类引用 = (子类类型)父类引用;
- 场景:当需要调用子类特有的方法时,需将向上转型后的父类引用强制转为子类引用;
- 风险:若父类引用指向的对象不是目标子类类型,会抛出
ClassCastException
(类型转换异常),因此需先用instanceof
关键字判断类型; - 实例:
// 向上转型(父类引用指向Dog对象) Animal animal = new Dog("旺财", 3, "中华田园犬");// 先用instanceof判断:animal是否是Dog类型 if (animal instanceof Dog) {// 向下转型:强制转为Dog类型Dog dog = (Dog) animal;dog.bark(); // 调用子类特有方法(输出:旺财(中华田园犬)正在叫:汪汪汪!) }// 错误示例:父类引用指向Cat对象,强制转为Dog会报错 Animal animal2 = new Cat("咪宝", 2); if (animal2 instanceof Dog) {Dog dog2 = (Dog) animal2; // 条件不满足,不会执行,避免异常 }
instanceof
关键字:判断对象类型
- 语法:
对象 instanceof 类/接口
,返回boolean
值(true
表示对象是该类/接口的实例,或其子类实例); - 作用:避免向下转型时的
ClassCastException
,是多态中安全转型的必要步骤。
3.5.3 多态的实际应用场景
多态的核心价值是“解耦”,降低代码的耦合度,提升扩展性,常见应用场景:
1. 多态参数(方法参数用父类类型,接收所有子类对象)
// 方法参数为父类Animal类型,可接收Dog、Cat等所有子类对象
public static void feedAnimal(Animal animal, String food) {System.out.println("给" + animal.getName() + "喂" + food);animal.eat(); // 调用子类重写的eat()方法,多态体现
}// 测试:传入不同子类对象
public static void main(String[] args) {Dog dog = new Dog("旺财", 3, "中华田园犬");Cat cat = new Cat("咪宝", 2);feedAnimal(dog, "骨头"); // 输出:给旺财喂骨头 → 旺财正在吃食物 → 它最喜欢吃骨头feedAnimal(cat, "小鱼干");// 输出:给咪宝喂小鱼干 → 咪宝正在吃食物 → 它最喜欢吃小鱼干
}
2. 多态数组(数组元素为父类类型,存储所有子类对象)
public static void main(String[] args) {// 多态数组:Animal数组存储Dog和Cat对象Animal[] animals = new Animal[2];animals[0] = new Dog("旺财", 3, "中华田园犬");animals[1] = new Cat("咪宝", 2);// 遍历数组,调用每个对象的eat()方法(多态体现)for (Animal animal : animals) {animal.eat();// 若为Dog对象,调用bark()方法(需向下转型)if (animal instanceof Dog) {((Dog) animal).bark();}}
}
3. 接口多态(接口引用指向实现类对象,最常用的多态形式)
// 定义接口(规范)
public interface Flyable {void fly(); // 抽象方法(接口中默认public abstract)
}// 实现类1:Bird实现Flyable
public class Bird implements Flyable {@Overridepublic void fly() {System.out.println("小鸟扇动翅膀飞行");}
}// 实现类2:Plane实现Flyable
public class Plane implements Flyable {@Overridepublic void fly() {System.out.println("飞机靠引擎飞行");}
}// 测试:接口多态
public class FlyableTest {public static void main(String[] args) {Flyable flyable1 = new Bird(); // 接口引用指向Bird对象Flyable flyable2 = new Plane(); // 接口引用指向Plane对象flyable1.fly(); // 输出:小鸟扇动翅膀飞行flyable2.fly(); // 输出:飞机靠引擎飞行}
}
3.6 抽象类与接口:OOP的“抽象与规范”
抽象类和接口是OOP中用于“抽象共性”和“定义规范”的重要结构,二者都不能实例化,需通过子类/实现类使用,但适用场景不同。
3.6.1 抽象类:“半抽象”的模板
抽象类是指用abstract
关键字修饰的类,它包含“抽象方法”(无实现的方法)和“普通方法”(有实现的方法),用于抽取子类的共性,同时保留部分实现逻辑。
抽象类的定义与规则:
- 定义语法:
public abstract class 类名 {}
; - 抽象方法:用
abstract
修饰的方法,只有方法签名,无方法体(public abstract 返回值类型 方法名(参数);
); - 核心规则:
- 抽象类不能实例化(不能
new 抽象类()
); - 若子类继承抽象类,必须重写所有抽象方法(除非子类也是抽象类);
- 抽象类可包含普通方法、成员变量、构造方法(构造方法用于子类初始化,而非实例化抽象类);
- 抽象类不能用
final
修饰(final
类不能被继承,与抽象类的“子类重写”矛盾)。
- 抽象类不能实例化(不能
抽象类实例(Shape
抽象类):
// 抽象类:Shape(形状,抽取共性)
public abstract class Shape {protected String color; // 成员变量// 构造方法(用于子类初始化)public Shape(String color) {this.color = color;}// 抽象方法:计算面积(无实现,由子类根据形状实现)public abstract double calculateArea();// 抽象方法:计算周长(无实现)public abstract double calculatePerimeter();// 普通方法:有实现(所有形状的共性行为)public void showInfo() {System.out.println("形状颜色:" + this.color);System.out.println("面积:" + calculateArea()); // 调用抽象方法(子类重写后生效)System.out.println("周长:" + calculatePerimeter());}
}// 子类1:Circle(圆形,继承抽象类)
public class Circle extends Shape {private double radius; // 半径public Circle(String color, double radius) {super(color); // 调用父类抽象类的构造方法this.radius = radius;}// 重写抽象方法:计算圆面积(πr²)@Overridepublic double calculateArea() {return Math.PI * radius * radius;}// 重写抽象方法:计算圆周长(2πr)@Overridepublic double calculatePerimeter() {return 2 * Math.PI * radius;}
}// 子类2:Rectangle(矩形,继承抽象类)
public class Rectangle extends Shape {private double length; // 长private double width; // 宽public Rectangle(String color, double length, double width) {super(color);this.length = length;this.width = width;}// 重写抽象方法:计算矩形面积(长×宽)@Overridepublic double calculateArea() {return length * width;}// 重写抽象方法:计算矩形周长(2×(长+宽))@Overridepublic double calculatePerimeter() {return 2 * (length + width);}
}// 测试:抽象类的使用
public class ShapeTest {public static void main(String[] args) {// 多态:抽象类引用指向子类对象Shape circle = new Circle("红色", 5);Shape rectangle = new Rectangle("蓝色", 4, 6);circle.showInfo(); // 输出:形状颜色:红色 → 面积:78.54... → 周长:31.42...rectangle.showInfo();// 输出:形状颜色:蓝色 → 面积:24.0 → 周长:20.0}
}
3.6.2 接口:“完全抽象”的规范
接口是指用interface
关键字定义的结构,JDK8前仅包含“抽象方法”和“常量”,JDK8后支持“默认方法”(default
)和“静态方法”(static
),用于定义“行为规范”,不包含任何实现逻辑。
接口的定义与规则:
- 定义语法:
public interface 接口名 {}
; - 接口成员(JDK8+):
- 抽象方法:默认
public abstract
,可省略(如void fly();
等价于public abstract void fly();
); - 常量:默认
public static final
,必须初始化(如int MAX_SPEED = 100;
); - 默认方法:用
default
修饰,有方法体,用于接口升级(不影响实现类); - 静态方法:用
static
修饰,有方法体,通过“接口名.方法名”调用;
- 抽象方法:默认
- 核心规则:
- 接口不能实例化(不能
new 接口()
); - 类实现接口用
implements
关键字,可实现多个接口(class A implements B, C {}
,解决Java单继承问题); - 实现类必须重写接口的所有抽象方法(除非实现类是抽象类);
- 接口不能用
final
修饰(接口需被实现,final
矛盾); - 接口之间可继承(
interface A extends B, C {}
,支持多继承)。
- 接口不能实例化(不能
接口实例(Swimmable
接口):
// 自定义接口:Swimmable(游泳规范)
public interface Swimmable {// 常量(默认public static final)int MAX_DEPTH = 10; // 最大游泳深度// 抽象方法(默认public abstract)void swim();// 默认方法(JDK8+):有方法体,实现类可重写或直接使用default void showSwimInfo() {System.out.println("最大游泳深度:" + MAX_DEPTH + "米");System.out.println("正在游泳...");}// 静态方法(JDK8+):通过接口名调用static void tips() {System.out.println("游泳前请做好热身运动!");}
}// 实现类:Fish(鱼实现Swimmable接口)
public class Fish implements Swimmable {private String name;public Fish(String name) {this.name = name;}// 重写接口的抽象方法@Overridepublic void swim() {System.out.println(this.name + "用鱼鳍划水游泳");}// 可选:重写接口的默认方法@Overridepublic void showSwimInfo() {Swimmable.super.showSwimInfo(); // 调用接口默认方法System.out.println(this.name + "的游泳速度:1m/s");}
}// 测试:接口的使用
public class SwimmableTest {public static void main(String[] args) {// 接口多态:接口引用指向实现类对象Swimmable fish = new Fish("小丑鱼");fish.swim(); // 输出:小丑鱼用鱼鳍划水游泳fish.showSwimInfo(); // 输出:最大游泳深度:10米 → 正在游泳... → 小丑鱼的游泳速度:1m/s// 调用接口静态方法(接口名.方法名)Swimmable.tips(); // 输出:游泳前请做好热身运动!// 访问接口常量(接口名.常量名)System.out.println("接口常量:" + Swimmable.MAX_DEPTH); // 输出:10}
}
3.6.3 抽象类与接口的区别(核心对比)
对比维度 | 抽象类(Abstract Class) | 接口(Interface) |
---|---|---|
关键字 | abstract class | interface |
成员组成 | 抽象方法、普通方法、成员变量、构造方法、静态方法 | 抽象方法、常量、默认方法(JDK8+)、静态方法(JDK8+) |
实例化 | 不能实例化 | 不能实例化 |
继承/实现 | 子类用extends继承,单继承 | 类用implements实现,多实现(支持多个接口) |
访问修饰符 | 成员可private、protected、public等 | 抽象方法默认public,常量默认public static final |
核心作用 | 抽取子类共性,包含部分实现逻辑(模板) | 定义行为规范,不包含实现(解耦) |
适用场景 | 子类有共性属性和部分共性实现(如Shape、Animal) | 不同类有相同行为但无共性属性(如Flyable、Swimmable) |
3.7 内部类:“类中的类”
内部类是指定义在另一个类(外部类)内部的类,它可访问外部类的成员(包括private
成员),常用于实现“逻辑内聚”(内部类仅服务于外部类)。Java中内部类分为4种:成员内部类、局部内部类、匿名内部类、静态内部类。
3.7.1 成员内部类:外部类的“成员”
- 定义位置:外部类的成员位置(与成员变量、成员方法同级);
- 访问规则:
- 内部类可直接访问外部类的所有成员(包括
private
); - 外部类访问内部类成员,需先创建内部类对象;
- 内部类可直接访问外部类的所有成员(包括
- 创建方式:
外部类对象.new 内部类();
。
成员内部类实例:
// 外部类:Outer
public class Outer {private String outerName = "外部类名称";// 成员内部类:Innerpublic class Inner {private String innerName = "内部类名称";// 内部类方法:访问外部类成员public void showOuterMember() {// 直接访问外部类的private成员System.out.println("外部类成员:" + outerName);}// 内部类方法:访问内部类成员public void showInnerMember() {System.out.println("内部类成员:" + innerName);}}// 外部类方法:访问内部类成员public void accessInner() {// 需先创建内部类对象Inner inner = new Inner();inner.showInnerMember();}
}// 测试:成员内部类的使用
public class InnerClassTest {public static void main(String[] args) {// 1. 创建外部类对象Outer outer = new Outer();// 2. 创建内部类对象(外部类对象.new 内部类())Outer.Inner inner = outer.new Inner();// 3. 调用内部类方法inner.showOuterMember(); // 输出:外部类成员:外部类名称inner.showInnerMember(); // 输出:内部类成员:内部类名称// 4. 调用外部类方法(间接访问内部类)outer.accessInner(); // 输出:内部类成员:内部类名称}
}
3.7.2 匿名内部类:“没有名字的内部类”
- 定义位置:方法内部或表达式中,是局部内部类的特殊形式(无类名);
- 核心特点:
- 必须继承一个类或实现一个接口;
- 只能使用一次(创建对象时定义,无法重复使用);
- 语法简洁,常用于“接口回调”“事件监听”等场景;
- 语法:
new 父类/接口() { 重写方法 };
。
匿名内部类实例(接口回调):
// 定义接口:Greeting
public interface Greeting {void sayHello(String name);
}// 测试:匿名内部类的使用
public class AnonymousInnerTest {// 方法:接收Greeting接口参数public static void showGreeting(Greeting greeting, String name) {greeting.sayHello(name);}public static void main(String[] args) {// 1. 匿名内部类:实现Greeting接口,创建对象并传入方法showGreeting(new Greeting() {// 重写接口的抽象方法@Overridepublic void sayHello(String name) {System.out.println("你好," + name + "!(匿名内部类实现)");}}, "张三");// 2. 对比:非匿名实现类(需单独定义类,繁琐)Greeting greeting = new NormalGreeting();showGreeting(greeting, "李四");}
}// 非匿名实现类(对比用)
class NormalGreeting implements Greeting {@Overridepublic void sayHello(String name) {System.out.println("你好," + name + "!(非匿名实现类)");}
}
输出结果:
你好,张三!(匿名内部类实现)
你好,李四!(非匿名实现类)
3.7.3 静态内部类:“外部类的静态成员”
- 定义位置:外部类的成员位置,用
static
修饰; - 访问规则:
- 静态内部类只能访问外部类的静态成员(不能访问非静态成员,因静态内部类不依赖外部类对象);
- 外部类访问静态内部类,无需创建外部类对象;
- 创建方式:
外部类名.内部类名 变量名 = new 外部类名.内部类名();
。
静态内部类实例:
// 外部类:Outer
public class Outer {// 外部类静态成员private static String staticOuterName = "外部类静态成员";// 外部类非静态成员(静态内部类不可访问)private String nonStaticOuterName = "外部类非静态成员";// 静态内部类:用static修饰public static class StaticInner {private String innerName = "静态内部类成员";// 静态内部类方法:访问外部类静态成员public void showOuterStaticMember() {System.out.println("外部类静态成员:" + staticOuterName);// System.out.println(nonStaticOuterName); // 错误:静态内部类不能访问外部类非静态成员}// 静态内部类方法:访问自身成员public void showInnerMember() {System.out.println("静态内部类成员:" + innerName);}}
}// 测试:静态内部类的使用
public class StaticInnerTest {public static void main(String[] args) {// 创建静态内部类对象(无需创建外部类对象)Outer.StaticInner staticInner = new Outer.StaticInner();staticInner.showOuterStaticMember(); // 输出:外部类静态成员:外部类静态成员staticInner.showInnerMember(); // 输出:静态内部类成员:静态内部类成员}
}
3.8 OOP开发注意事项(避坑指南)
-
构造方法调用顺序问题:
- 子类构造默认调用父类无参构造,若父类仅定义了带参构造(无无参构造),子类必须显式用
super(参数)
调用父类带参构造,否则编译报错; - 解决:父类手动定义无参构造,或子类构造第一行显式调用父类带参构造。
- 子类构造默认调用父类无参构造,若父类仅定义了带参构造(无无参构造),子类必须显式用
-
方法重写与静态方法的区别:
- 父类静态方法不能被重写,子类若定义同名静态方法,是“隐藏”而非“重写”(调用时取决于引用类型,而非对象类型);
- 例:
Animal animal = new Dog(); animal.staticMethod();
调用父类Animal
的静态方法,而非子类Dog
的。
-
多态向下转型的异常风险:
- 向下转型前必须用
instanceof
判断对象类型,避免ClassCastException
; - 错误:
Animal animal = new Cat(); Dog dog = (Dog) animal;
(无instanceof
,直接转型报错); - 正确:
if (animal instanceof Dog) { Dog dog = (Dog) animal; }
。
- 向下转型前必须用
-
接口多实现的默认方法冲突:
- 若一个类实现的多个接口有同名默认方法,该类必须重写该方法,否则编译报错;
- 例:
interface A { default void show() {} }
,interface B { default void show() {} }
,class C implements A, B
必须重写show()
。
-
内部类访问外部类变量的限制:
- JDK8前,局部内部类/匿名内部类访问外部类的局部变量,需用
final
修饰; - JDK8后,虽可省略
final
,但变量必须是“effectively final”(即赋值后不再修改),否则编译报错。
- JDK8前,局部内部类/匿名内部类访问外部类的局部变量,需用
-
封装的“过度与不足”:
- 不足:成员变量用
public
修饰,直接暴露给外部,失去数据保护; - 过度:所有成员变量都用
private
,且无必要的getter/setter
(如仅内部使用的变量),增加代码冗余; - 原则:仅对外暴露必要的接口,隐藏不必要的细节。
- 不足:成员变量用
-
继承的“滥用”:
- 不要为了代码复用而盲目继承(如
Dog extends Person
,逻辑上不成立); - 继承的前提是“is-a”关系(如
Dog is a Animal
),若仅需复用方法,优先用“组合”(将父类作为子类的成员变量)而非继承。
- 不要为了代码复用而盲目继承(如