本文纲要
- 继承入门
- 继承的好处与弊端
- 继承的特点
- 继承中成员变量的访问特点
- this 和 super 访问成员的格式
- 继承中成员方法的访问特点
- 方法重写概述与应用场景
- 方法重写的注意事项
- 权限修饰符
- 继承中构造方法的访问特点
- 构造方法的访问特点 - 父类没有无参构造
- 代码优化与内存图解
- 信息管理系统- 集成改进
- 抽象类入门
- 抽象类的注意事项
- 模板设计模式
- final 关键字
- 信息管理系统- 抽象类改进
- 代码块
- 信息管理系统- 代码块改进
- 总结
本文代码结构
project-root/
├── test-abstract/src/com/wb (代码块)
│ ├── mfinal/ (final关键字)
│ ├── test1/ (抽象类)
│ └── test2/ (模板设计模式)
├── test-extends/src/com/wb (继承)
│ ├── constructor/ (继承-构造方法)
│ ├── override/ (方法重写)
│ ├── override2/ (方法重写注意事项)
│ ├── test1/ (继承-基础)
│ ├── test2/ (继承-优化)
│ ├── test3/ (继承-多层继承)
│ ├── test4/ (继承-成员变量访问)
│ └── test5/ (继承-成员方法访问)
├── test-permission/src/com/wb/ (代码块)
│ ├── test1/ (权限修饰符)
│ └── test2/ (权限修饰符-不同包)
├── test-block/src/com/wb/block (代码块)
│ ├── construction/ (构造代码块)
│ ├── local/ (局部代码块)
│ └── mstatic/ (静态代码块)
└── wb-edu-info-manager/com/wb/edu/info/manager/ (信息管理系统)
├── controller/ (控制层)
├── dao/ (数据访问层)
├── domain/ (实体类)
├── entry/ (程序入口)
├── factory/ (工厂类)
└── service/ (业务层)
继承入门
概念: 继承是面向对象编程的第二大特征(封装、继承、多态)。它允许类与类之间产生子父类关系。子类可以直接使用父类中非私有的成员变量和成员方法。
场景: 在之前的黑马信息管理系统中,Student类和Teacher类中存在大量共性的代码(如 id, name, age 等属性),这降低了代码的复用性。为了解决这个问题,可以将这些共性的内容向上抽取,形成一个父类 Person,然后让 Student 和 Teacher 继承它。
格式: 在类名后使用 extends 关键字,后跟父类名
public class 子类名 extends 父类名 { ... }
代码演示:
// 父类 Person,抽取了 Student 和 Teacher 的共性内容
public class Person {
private String id;
private String name;
private String age;
private String birthday;
public Person() {
}
public Person(String id, String name, String age, String birthday) {
this.id = id;
this.name = name;
this.age = age;
this.birthday = birthday;
}
// 提供非私有的 get/set 方法供子类访问
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
// 其他 get/set 方法...
}
// 子类 Student,通过 extends 关键字继承 Person
public class Student extends Person {
// 子类可以编写自己特有的成员,没有编写任何代码,也可以使用父类非私有成员
}
// 子类 Teacher
public class Teacher extends Person {
// ...
}
结论: 虽然父类中的成员变量是 private 的,子类不能直接访问,但父类提供了非私有的 getter/setter 方法。子类通过继承这些方法,间接地访问了父类的私有数据。
继承的好处与弊端
好处:
-
提高代码的复用性: 这是继承最直观的好处,子类无需重复编写代码,可以直接使用父类的非私有成员。
-
提高代码的维护性: 当需求变更(如增加一个属性),只需在父类中修改一次,所有子类都会自动更新。
// 没有继承时,修改需求需在多个类中重复添加属性 public class Student { / ... / String address; } public class Teacher { / ... / String address; }// 有继承时,只在父类修改一次 public class Person { / ... / String address; } // Student 和 Teacher 无需任何修改,即可使用 address 属性(通过get/set) -
是多态的前提: 代码之间的“是”(is-a)关系(学生是人的一种)是建立继承关系的基础,这也是多态技术实现的前提。
弊端:
- 降低了代码的灵活性: 继承是侵入性的。子类必须拥有父类非私有的属性和方法,这给予了子类一些不想要的约束。
- 增强了代码的耦合性: 父类和子类之间的关联非常紧密(强耦合)。如果父类的某个成员被修改或删除,所有使用该成员的子类代码都可能需要修改,否则会报错,这就是“牵一发而动全身”。
应用场景: 当类与类之间存在共性内容,并且满足"is-a"关系时(如:学生和老师都是人),就可以考虑使用继承来优化代码结构。
继承的特点
-
单继承: Java只支持单继承,一个子类只能有一个直接父类。
-
不支持多继承: 一个子类不能同时继承多个父类。
// 错误写法!Java不支持多继承 public class C extends A, B { }原因:如果父类A和B有同名方法但实现不同,子类调用时会产生逻辑冲突,Java为避免这种二义性,不支持多继承。
-
多层继承: 支持多层继承体系(子->父->爷)。
public class A { public void methodA(){} } public class B extends A { public void methodB(){} } // C 是最底层的子类,可以访问 A 和 B 中所有非私有成员 public class C extends B { } public class Test { public static void main(String[] args) { C c = new C(); c.methodA(); // 访问爷爷类的方法 c.methodB(); // 访问父亲类的方法 } }
继承中成员变量的访问特点
访问一个变量时,遵循就近原则:
- 先在方法内部查找(局部变量)。
- 没有则去子类的成员范围查找(本类成员变量)。
- 还没有则去父类的成员范围查找(父类成员变量)。
- 如果都找不到,则编译报错。
子父类成员变量重名:
如果子类和父类出现了重名的成员变量,默认访问的是子类的。如果想在子类中访问父类的同名变量,需要使用 super 关键字。
public class Fu {
int a = 10;
}
public class Zi extends Fu {
int a = 20; // 与父类重名
public void method() {
int a = 30; // 局部变量
System.out.println(a); // 30,就近使用局部变量
System.out.println(this.a); // 20,使用this区分,访问本类成员变量
System.out.println(super.a); // 10,使用super关键字访问父类成员变量
}
}
this 和 super 访问成员的格式
this: 代表本类对象的引用。用于区分局部变量和成员变量,或调用本类的其他成员。
super: 代表父类存储空间的标识(可以理解为父类对象的引用)。用于在子类中访问父类的成员。
两者的格式非常相似,可以调用成员变量、成员方法和构造方法。
| 关键字 | 访问成员变量 | 访问成员方法 | 访问构造方法 |
|---|---|---|---|
| this | this.成员变量 | this.成员方法(参数) | this(参数) |
| super | super.成员变量 | super.成员方法(参数) | super(参数) |
| this(…) 和 super(…) 是用来调用构造方法的,且都必须放在构造方法的第一行。 |
继承中成员方法的访问特点
通过子类对象访问方法时,查找顺序:
- 在子类的成员范围查找。
- 如果子类有,则执行子类的方法。
- 如果子类没有,则去父类的成员范围查找。
- 如果父类也没有,则报错。
代码演示:
public class Fu {
public void show() { System.out.println("父类show方法"); }
}
public class Zi extends Fu {
// 子类有 show 方法,则调用时执行这个
public void show() { System.out.println("子类show方法"); }
public void method() {
this.show(); // 调用本类的show方法
super.show(); // 通过super调用父类的show方法
}
}
方法重写概述与应用场景
概念: 在继承体系中,子类出现了和父类一模一样的方法声明(方法名、参数列表、返回值类型都相同)的行为,称为方法重写 (Override)。
应用场景: 当子类需要父类的功能,但功能主体子类又有自己特有的实现时。通过重写,既能沿袭父类的功能,又能定义子类特有的内容。
案例: 手机系统升级。v1.0 手机的语音助手 smallBlack 只说英文,v2.0 要求既能说英文又能说中文。
// 父类 iPearV1
public class iPearV1 {
public void smallBlack() {
System.out.println("speak english...");
}
}
// 子类 iPearV2
public class iPearV2 extends iPearV1 {
// 重写父类方法
@Override
public void smallBlack() {
// 调用父类的功能,沿袭了说英文的能力
super.smallBlack();
// 添加子类特有的功能
System.out.println("说中文");
}
}
区分方法重载 (Overload):
- 重写 (Override): 在继承体系的子父类中,方法声明一模一样。
- 重载 (Overload): 在同一个类中,方法名相同,参数列表不同,与返回值无关。
方法重写的注意事项
- 权限:子类重写方法时,访问权限必须大于等于父类方法的权限。
- 静态:父类中静态方法,不能被子类以非静态方法重写,反之亦然。静态方法本身不能被重写(只能被隐藏),但可以用 @Override 注解来检查。
- 私有:父类中私有方法不能被重写。
public class Fu {
void method() { ... } // 默认权限
// public static void show() { ... }
}
public class Zi extends Fu {
@Override
void method() { ... } // OK, 权限 >= 默认权限
// @Override
// public void show() { ... } // 报错,父类静态,子类未用 static
}
权限修饰符
Java中有四种权限修饰符,用来控制成员的访问范围,从小到大的等级为:
private < 默认 (什么都不写) < protected < public
| 修饰符 | 同一个类 | 同一个包中的子类/无关类 | 不同包下的子类 | 不同包下的无关类 |
|---|---|---|---|---|
| private | √ | × | × | × |
| 默认 (default) | √ | √ | × | × |
| protected | √ | √ | √ | × |
| public | √ | √ | √ | √ |
我们日常开发中最常用的是 private(用于封装)和 public(对外提供接口)。
继承中构造方法的访问特点
规则:子类中所有的构造方法,默认都会先访问父类中的无参构造方法,再执行自己的方法体。
为什么?: 因为子类在初始化时,可能会使用到父类的数据。如果父类数据没有完成初始化,子类就无法安全地使用它。所以,子类初始化前,必须先完成父类的初始化。
**怎么做? **:super() 语句。每一个子类构造方法的第一行,都隐式地有一条 super() 语句,用于调用父类的无参构造。
public class Person {
public Person() {
System.out.println("父类无参构造");
}
}
public class Student extends Person {
public Student() {
// 系统默认隐藏了 super();
System.out.println("子类无参构造");
}
}
// main中执行 new Student();
// 控制台输出顺序:
// "父类无参构造"
// "子类无参构造"
Object类:每一个类都直接或间接地继承了 Object 类。Object 是 Java 类层次结构的最顶层。父类 Person 如果不显式继承其他类,那么它也默认继承 Object。Person 构造中的 super() 调用的就是 Object 的无参构造。
构造方法的访问特点 - 父类没有无参构造
如果父类没有提供无参构造方法,只提供了有参构造方法,子类构造方法就不会再默认生成 super(),而是会报错。
解决方案(推荐):
在子类构造方法中,手工调用父类的有参构造方法 super(参数),并传入相应的参数。
public class Fu {
private int age;
// 父类没有无参构造
public Fu(int age) {
this.age = age;
}
}
public class Zi extends Fu {
public Zi(int age) {
super(age); // 必须手写,显式调用父类的有参构造
}
}
注意事项:
this(...) 和 super(...) 调用构造方法的语句,都必须放在构造方法的第一行有效语句。因此,它们在同一个构造方法中不能共存。
代码优化与内存图解
在编写继承代码时,子类的构造方法可以通过快捷键 Alt + Insert (或右键 -> Generate…) 快速生成。如果需要初始化完整的属性,子类的有参构造应接收所有数据(包括父类的),并通过 super 将父类的数据传递给父类去初始化。
// 父类
public class Person {
private String name;
private int age;
public Person(String name, int age) { this.name = name; this.age = age; }
// ...
}
// 子类
public class Student extends Person {
private int score;
public Student(String name, int age, int score) {
super(name, age); // 将 name 和 age 传递给父类初始化
this.score = score; // 子类自己初始化独有属性
}
}
内存图解(堆内存):
当创建 new Student(“张三”, 23, 100) 时,堆内存的结构分析。
结论:
父类中private的成员也会在子类对象的super区域内有一份存储空间。子类只是没有直接访问的权限。
初始化是分层进行的:先由父类完成其成员的初始化,再由于子类完成其自身特有成员的初始化。
信息管理系统 - 集成改进
利用继承思想对信息管理系统进行优化。
1 ) 实体类优化:将 Student 和 Teacher 的共性属性抽取到 Person 父类。
2 ) 控制器优化:
- 起初,StudentController 使用 set方法给属性赋值,代码冗长。
- 新业务要求使用构造方法赋值,但不希望修改原有代码。这体现了开闭原则(对修改关闭,对扩展开放)。
- 解决方案:创建一个新控制器 OtherStudentController,复写 inputStudentInfo 方法,在其内部使用有参构造来封装 Student 对象。
- 进一步优化:发现 StudentController 和 OtherStudentController 中有大量共性代码。于是向上抽取,创建一个抽象的 BaseStudentController 父类,将公共方法(如 start, addStudent, deleteStudentById 等)放入其中。子类只需专注于重写 inputStudentInfo 这个核心差异方法。
优化后的代码结构:
// 抽象的父类控制器
public abstract class BaseStudentController {
private StudentService studentService = new StudentService();
private Scanner sc = new Scanner(System.in);
public final void start() { / 复杂的控制逻辑, 不允许子类修改 / }
public final void deleteStudentById() { / ... / }
public final void addStudent() {
// ... 接收id等公共逻辑 ...
Student stu = inputStudentInfo(id); // 调用抽象方法,具体怎么输入由子类决定
// ...
}
// 将录入学生信息的方法定义为抽象,强制子类实现,体现了多态思想
public abstract Student inputStudentInfo(String id);
}
// 子类实现1: 使用set方法赋值
public class StudentController extends BaseStudentController {
@Override
public Student inputStudentInfo(String id) {
// ... 键盘录入 ...
Student stu = new Student();
stu.setId(id);
stu.setName(name);
// ...
return stu;
}
}
// 子类实现2: 使用构造方法赋值
public class OtherStudentController extends BaseStudentController {
@Override
public Student inputStudentInfo(String id) {
// ... 键盘录入 ...
Student stu = new Student(id, name, age, birthday);
return stu;
}
}
抽象类入门
1 ) 概念由来: 当我们将共性方法(如 eat)向上抽取到父类 Animal 时,发现该方法在父类中无法给出具体的实现(因为猫吃鱼,狗吃肉,方法体不同)。这种只有方法声明而没有方法体的,就是抽象方法。有抽象方法的类,必须是抽象类。
2 ) 格式:
抽象方法:public abstract void 方法名(参数列表);
抽象类:public abstract class 类名 { ... }
3 ) 案例:
// 父类是抽象类
public abstract class Animal {
// 普通方法,可以直接有实现
public void drink() { System.out.println("喝水"); }
// 抽象方法,没有方法体
public abstract void eat();
}
// 子类继承抽象类后,必须重写所有抽象方法
public class Cat extends Animal {
@Override
public void eat() { System.out.println("猫吃鱼"); }
}
public class Dog extends Animal {
@Override
public void eat() { System.out.println("狗吃肉"); }
}
抽象类的注意事项
1 ) 不能实例化:抽象类不能创建对象(new Animal() 是错误的)。因为如果允许创建,就可以调用没有方法体的抽象方法,这是没有意义的。
2 ) 有构造方法:抽象类中有构造方法。这是为了让子类通过 super() 调用,以完成父类成员的初始化。
3 ) 子类的职责:
方案A(推荐):子类必须重写抽象父类中的所有抽象方法。
方案B(不推荐):把自己也变成一个抽象类。但这会导致功能延期,最终必须由另一个子类来实现抽象方法,增加了复杂度。
4 ) 抽象类中可以有普通方法:抽象类中可以没有抽象方法。但有抽象方法的类,必须是抽象类。
模板设计模式
1 ) 设计模式: 一套被反复使用、多数人知晓、经过分类编目的代码设计经验的总结。简单说,就是一种优秀的、固定的编码风格,而不是新技术点。
2 ) 模板设计模式: 抽象类最常见的应用。它将抽象类看成一个模板,用final修饰的普通方法作为模板骨架(定义好流程),而将流程中不能确定的部分定义为抽象方法,让子类来重写实现。
3 ) 案例: 作文模板。所有作文的标题和结尾是固定的(模板),但正文内容由不同学生自己决定
// 作文模板抽象类
public abstract class CompositionTemplate {
// final 修饰的模板方法,定义了写作文的整体骨架,不允许子类修改
public final void write() {
System.out.println("<<我的爸爸>>"); // 固定部分
body(); // 不确定部分,由子类实现
System.out.println("啊~ 这就是我的爸爸"); // 固定部分
}
// 抽象方法,将变化的正文内容留给子类实现
public abstract void body();
}
// Tom 同学的作文
public class Tom extends CompositionTemplate {
@Override
public void body() {
System.out.println("那是一个秋天, 风儿那么缠绵...");
}
}
// 测试
public class Test { public static void main(String[] args) { new Tom().write(); } }
final 关键字
final 是最终的意思,可用于修饰类、方法和变量。
1 ) 修饰类:该类是最终类,不能被继承。例如,当希望一个工具类的所有方法都不能被重写时,可以直接将类声明为 final
java public final class Fu { } // public class Zi extends Fu { } // 报错
2 ) 修饰方法:该方法是最终方法,不能被重写。常用于模板方法(如上文的 write())。
public class Fu { public final void show(){} }
// public class Zi extends Fu { public void show(){} } // 报错
3 ) 修饰变量:变量变为常量,只能被赋值一次。
-
基本数据类型:数据值不可变。
-
引用数据类型:对象的内存地址不可变,但对象内部属性可以修改。
// 常量命名规范: 单词全大写,多个单词用_分隔 public static final int MAX_VALUE = 100; final Student stu = new Student("张三"); // 地址值被绑定,不能修改 stu.setName("李四"); // OK, 对象内容可以修改 // stu = new Student("李四"); // Error, 不能新指向其他对象 -
修饰成员变量时:必须在对象构造完成前完成初始化。有两种方式:
- 在定义时直接赋值:final int a = 10;
- 在构造方法中完成赋值。
信息管理系统 - 抽象类改进
运用抽象类思想,对 BaseStudentController 继续优化
- 将
inputStudentInfo设计为抽象方法,因为它就是模板中无法确定的部分 - 将类本身和内部不希望子类修改的核心方法(如 start, addStudent 等)用 abstract 和 final 进行修饰,强化模板设计的规范性和安全性
public abstract class BaseStudentController {
// ... 其他final方法 ...
public final void start() { ... }
public abstract Student inputStudentInfo(String id);
}
代码块
Java中用 {} 括起来的代码段就是代码块,根据位置和修饰符不同,分为三种
1 ) 局部代码块
位置:方法内部
作用:限定变量的生命周期,让变量尽早释放,提高内存利用率
public void method() {
{
int a = 10; // a 的作用域只在这个大括号内
System.out.println(a);
}
// System.out.println(a); // 无法访问,a已从内存释放
}
2 ) 构造代码块
位置:类中方法外,无任何修饰符。
特点:每次构造方法执行时,都会在构造方法体之前执行。
作用:将多个构造方法中相同的代码抽取出来,提高代码复用性。
class Student {
{ System.out.println("好好学习"); } // 构造代码块
public Student() { System.out.println("空参构造"); }
public Student(int a) { System.out.println("带参构造"); }
}
// 每次 new Student(),都会先打印“好好学习”,再打印各自的构造方法内容
3 ) 静态代码块
位置:类中方法外,用 static 修饰。
特点:随着类的加载而加载,且只执行一次,优先于对象的创建。
作用:在类加载时,做一些数据初始化操作
class Person {
static { System.out.println("我是静态代码块, 类一加载我就执行"); }
public Person() { System.out.println("构造方法执行"); }
}
// 执行 new Person() 两次,静态代码块只在第一次加载类时执行
信息管理系统 - 代码块改进
为了在系统启动后,自动拥有初始学生数据,方便功能测试,我们可以利用静态代码块来初始化 StudentDao 中的学生数组/集合。
public class StudentDao implements BaseStudentDao {
// 创建学生对象数组
private static Student[] stus = new Student[5];
// 静态代码块: 初始化数组数据
static {
Student stu1 = new Student("heima001", "张三", "23", "1999-11-11");
Student stu2 = new Student("heima002", "李四", "24", "2000-11-11");
stus[0] = stu1;
stus[1] = stu2;
}
// ...
}
这样,当 StudentDao 类被加载到JVM时,静态代码块执行,学生数组就自动添加了初始数据,大大方便了后续的开发和测试
总结
1 ) 继承 — 代码复用的基石
- 概念:子类通过 extends 关键字获取父类的非私有成员。
- 优点:提高代码复用性、维护性,是多态的前提。
- 弊端:侵入性强,耦合性高,使用需谨慎。
- 特点:Java 单继承、多层继承,不支持多继承(避免方法冲突)。
- 成员访问:
- 变量:就近原则(局部 → 本类成员 → 父类成员),同名时用 super. 访问父类变量。
- 方法:子类优先,用 super. 可显式调用父类方法。
- 构造方法:子类构造默认 super() 调用父类无参构造,用于完成父类数据初始化。
- 方法重写:子类定义与父类方法签名完全相同的方法,用于扩展或改变父类行为。
- 要求:权限不降低,静态只能重写静态,私有不可重写。
- 与重载的区别:重载在同类的同名不同参。
2 ) 抽象类 — 模板与约束
- 抽象方法:只有声明,没有方法体,用 abstract 修饰。
- 抽象类:包含抽象方法的类必须声明为抽象类,也可包含普通方法。
- 规则:
- 不能实例化,但有构造方法(供子类 super() 用)。
- 子类必须实现所有抽象方法,或自己也声明为抽象类。
- 设计模式 — 模板模式:抽象类作为模板,定义骨架流程(final 方法),将可变部分声明为抽象方法,交由子类实现。保证整体结构统一,管理灵活。
3 ) final 关键字 — 不可变性
- 修饰类:类不能被继承。
- 修饰方法:方法不能被重写。
- 修饰变量:
- 基本类型值不可变,引用类型地址不可变(属性可变)。
- 成员变量须在构造完成前初始化(直接赋值或构造方法中赋值)。
- 常量命名全大写,多单词用下划线分隔。
4 )代码块 — 初始化利器
- 局部代码块(方法内):控制变量生命周期,优化内存。
- 构造代码块(类中方法外):每次构造方法执行前调用,提取公共初始化代码。
- 静态代码块(static 修饰):类加载时执行一次,适合静态数据初始化。
5 ) 权限修饰符 — 访问控制
从紧到宽:private → 默认 → protected → public。合理使用保护封装和继承可见性。
6 ) 实战:信息管理系统的优化
- 实体类:抽取 Person 父类,Student、Teacher 继承。
- 控制器层:
- 抽取 BaseStudentController 抽象父类,定义模板方法 start()、addStudent() 等并声明为 final。
- 将差异方法 inputStudentInfo() 声明为抽象方法,由子类 StudentController、OtherStudentController 分别实现。
- 符合开闭原则,新增子类时无需修改父类代码。
- 数据访问层:在 StudentDao 中使用静态代码块初始化初始学生数据,方便测试。
要领:面向对象编程中,通过继承提升复用,通过抽象规范行为,通过 final 锁定核心,通过代码块巧设初始化,最终构建出灵活、可维护的应用程序架构。
如果需要更简短的结论,一句话:以继承和抽象为核心的模板设计模式,结合 final 约束和各类代码块,能够有效提升 Java 项目代码的结构化、复用性和可维护性。
1075

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



