第五章 面向对象(进阶)
主要内容
- 面向对象编程-封装
- 面向对象编程-继承
- 方法的重写(override)
- super关键字
- 继承体系下,创建对象内存分析
- final关键字
- Object类
- 面向对象编程-多态
- 抽象类(abstract)
- 接口(interface)
- 内部类
学习目标
知识点 | 要求 |
---|---|
面向对象编程 - 封装 | 掌握 |
面向对象编程 - 继承 | 掌握 |
方法的重写(override) | 掌握 |
super 关键字 | 掌握 |
继承体系下,创建对象内存分析 | 理解 |
final 关键字 | 掌握 |
Object 类 | 掌握 |
面向对象编程 - 多态 | 掌握 |
抽象类(abstract) | 掌握 |
接口(interface) | 掌握 |
内部类 | 掌握 |
1. 封装(encapsulation)
1.1 封装的概念
面向对象编程有三大特性:封装、继承、多态。封装是这三大特性中最基础的一个概念。
1.1.1 封装的引入
想象一下,当我们使用电视机时,只需要按遥控器上的按钮就可以开关电视、调节音量或切换频道。我们不需要了解电视机内部的电路板、显像管或信号处理器是如何工作的。制造厂家将这些复杂的内部细节全部封装起来,只给我们提供简单易用的接口。
从Java的角度来看, 封装是指隐藏对象的属性和实现细节,仅对外提供公共的访问方式 。这样可以让使用者只关注对象提供了哪些功能,而不必关心这些功能是如何实现的。
1.1.2 封装的优点
- 提高代码的安全性 :防止外部代码直接访问和修改对象内部的数据,避免了不合理的操作。
- 提高代码的复用性 :封装后的代码更加模块化,便于在不同的场景中重复使用。
- 降低耦合度 :使用者只需要知道对象的访问方式,不需要了解内部实现细节,降低了代码之间的依赖关系。
- 提高可维护性 :当内部实现需要改变时,只要保持外部接口不变,使用者的代码就不需要修改。
1.1.3 封装的原则
- 将不需要对外提供的内容都隐藏起来 :使用私有访问修饰符(private)隐藏内部数据和实现细节。
- 把属性都隐藏,提供公共方法对其访问 :通过公共的
getter
和setter
方法来操作私有属性。
1.2 访问控制权限修饰符
Java中使用"访问控制修饰符"来控制哪些细节需要封装,哪些细节需要暴露。
下面详细讲述它们的访问权限范围,如下表所示:
修饰符 | 同一个类 | 同一个包 | 子类 | 所有类 |
---|---|---|---|---|
private | ✔ | |||
缺省(默认) | ✔ | ✔ | ||
protected | ✔ | ✔ | ✔ | |
public | ✔ | ✔ | ✔ | ✔ |
private:表示私有的,仅在当前类内部可见。
缺省:表示包级私有,在当前类和同一包内可见。
protected:表示受保护的,在当前类、同一包和不同包的子类中可见。
public:表示公开的,对所有类可见。
【注意事项】
- 类中的属性和方法访问权限共有四种:private、默认、protected和public。
- 类的访问权限只有两种:public和默认。
- 访问权限修饰符不能用于修饰局部变量。
1.3 成员变量封装
为什么要对成员变量的访问进行封装?让我们通过一个例子来说明没有封装的代码可能会出现的问题。
public class Student {// 成员变量String name;int age;// 成员方法public void show() {System.out.println("姓名: " + name + ",年龄: " + age);}
}
public class EncapsulationDemo {public static void main(String[] args) {Student stu = new Student();stu.name = "小明";// 年龄可以通过这种方式随意赋值,没有任何限制stu.age = -14;stu.show(); // 输出:姓名: 小明,年龄: -14}
}
在上面的代码中,我们可以直接给 age
属性赋一个负数,这显然不符合现实逻辑。年龄不可能是负数,也不可能超过一定的范围(比如130岁)。如果没有使用封装,外部代码可以给属性赋任意值,无法保证数据的合法性。
1.3.1 封装的实现方式
实现封装的标准方式是:
- 将类的属性声明为私有(private)
- 提供公共的
getter
和setter
方法来访问和修改这些属性 - 在
setter
方法中添加数据验证逻辑,确保属性值的合法性
关于使用权限修饰符的补充说明:
- 属性一般使用
private
访问权限,防止外部直接访问。 - 提供访问相关属性的
getter/setter
方法通常使用public
修饰,以方便对属性的合法赋值与读取操作。 - 一些只用于本类的辅助性方法可以用
private
修饰,希望其他类调用的方法用public
修饰。
1.4 JavaBean规范
JavaBean是一种可重用的Java组件,它是遵循特定规范的Java类:
- 类必须是具体的(非抽象的)和公共的
- 必须具有无参构造器
- 属性必须私有化,并提供公共的
getter
和setter
方法 - 实现Serializable接口(可选)
【示例】标准JavaBean的实现
public class Student {// 私有成员变量private String name;private int age;private boolean married; // 是否已婚// 无参构造器public Student() {}// 有参构造器public Student(String name, int age, boolean married) {this.name = name;this.age = age;this.married = married;}// getter和setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}// boolean类型的成员变量,getter方法通常以is开头public boolean isMarried() {return married;}public void setMarried(boolean married) {this.married = married;}
}
public class EncapsulationDemo {public static void main(String[] args) {Student stu = new Student();// 赋值操作// stu.name = "小明"; // 编译失败,无法直接访问私有属性stu.setName("小明");stu.setAge(18);stu.setMarried(false);// 取值操作// String name = stu.name; // 编译失败,无法直接访问私有属性String name = stu.getName();int age = stu.getAge();boolean isMarried = stu.isMarried();System.out.println("姓名: " + name + ",年龄: " + age + ",已婚: " + isMarried);// 输出:姓名: 小明,年龄: 18,已婚: false}
}
1.5 封装的实际应用
接下来使用封装来解决一下上面提到的年龄非法赋值的问题。
【示例】可见性封装的使用案例
public class Student {// 私有成员变量private String name;private int age;// 构造方法public Student() {}public Student(String name, int age) {this.name = name;// 调用setter方法来给年龄赋值,确保数据验证setAge(age);}// getter和setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}// 获取年龄时返回虚岁(实际年龄+1)public int getAge() {return age + 1; // 返回虚岁}// 设置年龄时进行数据验证public void setAge(int age) {// 在赋值之前先判断年龄是否合法if (age < 0 || age > 130) {System.out.println("年龄不合法,已设置为默认值0");this.age = 0; // 不合法时赋默认值0} else {this.age = age; // 合法才能赋值给属性}}// 获取实际年龄(不加1)的方法public int getRealAge() {return age;}// 显示学生信息public void show() {System.out.println("姓名: " + name + ",实际年龄: " + age + ",虚岁: " + getAge());}
}
public class EncapsulationDemo {public static void main(String[] args) {// 创建学生对象并使用setter方法赋值Student stu1 = new Student();stu1.setName("小明");stu1.setAge(150); // 尝试设置一个不合法的年龄stu1.show();System.out.println("------------------------");// 使用构造方法创建对象并赋值Student stu2 = new Student("小红", 16);stu2.show();// 获取学生信息String name = stu2.getName();int age = stu2.getAge(); // 获取虚岁int realAge = stu2.getRealAge(); // 获取实际年龄System.out.println("通过getter方法获取 - 姓名: " + name + ",虚岁: " + age + ",实际年龄: " + realAge);}
}
在这个改进的示例中:
- 我们将
age
属性设为私有,防止外部直接访问和修改 - 提供了
setAge()
方法,在赋值前验证年龄的合法性 - 提供了两个获取年龄的方法:
getAge()
返回虚岁,getRealAge()
返回实际年龄 - 在构造方法中也调用了
setAge()
方法,确保通过构造方法设置的年龄也经过验证
1.6 IDEA自动生成getter/setter方法
在实际开发中,大多数IDE(如IntelliJ IDEA、Eclipse等)都提供了自动生成getter和setter方法的功能,可以大大提高开发效率。
在IntelliJ IDEA中,可以使用快捷键Alt + Insert(Windows)或Command + N(Mac)打开生成菜单,然后选择"Getter and Setter"选项。
3. 继承(inheritance)
3.1 继承的概念
在面向对象编程中,继承是三大核心特性之一(封装、继承、多态)。继承建立了类与类之间的父子关系,使代码更具层次性和复用性。
在现实生活中,继承一般指的是子女继承父辈的财产或特征。在程序中,继承描述的是事物之间的所属关系,通过继承可以使多种事物之间形成一种层次分明的关系体系。
从英文字面意思理解, extends
的意思是"扩展"。子类是父类的扩展,现实世界中的继承无处不在,如下图所示:
上图中,狗继承了动物。这意味着,动物的特性,狗都具备;而在编程中,类的继承是指在一个现有类的基础上构建一个新的类,构建出来的新类被称作子类(派生类),现有类被称作父类(也称作超类、基类等)。子类会自动拥有父类所有可继承的属性和方法,同时还可以定义自己特有的属性和方法。
3.2 继承的使用
继承是面向对象最显著的一个特性。通过继承,新的类能吸收已有类的数据属性和行为,并能扩展新的能力,提高了代码的复用性。
在Java中,实现继承关系的语法如下:
class 父类 {// 父类的属性和方法
}class 子类 extends 父类 {// 子类的属性和方法
}
【示例】使用extends实现继承
// 父类:Person类
class Person {// 成员变量private String name;private int age;// 构造方法public Person() {}public Person(String name, int age) {this.name = name;this.age = age;}// getter和setter方法public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}// 成员方法public void eat() {System.out.println(name + "正在吃饭...");}public void sleep() {System.out.println(name + "正在睡觉...");}
}
// 子类:Teacher类
class Teacher extends Person {// 子类特有的成员变量private String subject; // 教授科目private String title; // 职称// 子类的构造方法public Teacher() {}public Teacher(String name, int age, String subject, String title) {// 调用父类的构造方法,初始化父类成员变量super(name, age);// 初始化子类特有的成员变量this.subject = subject;this.title = title;}// 子类特有属性的getter和setter方法public String getSubject() {return subject;}public void setSubject(String subject) {this.subject = subject;}public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}// 子类特有的方法public void teach() {System.out.println(getName() + "正在教授" + subject + "课程");}public void research() {System.out.println(getTitle() + getName() + "正在进行学术研究");}
}
// 测试类
public class InheritanceDemo {public static void main(String[] args) {// 创建Teacher对象Teacher teacher = new Teacher();// 设置父类继承的属性teacher.setName("张教授");teacher.setAge(45);// 设置子类特有的属性teacher.setSubject("Java编程");teacher.setTitle("副教授");// 调用父类继承的方法teacher.eat(); // 输出:张教授正在吃饭...teacher.sleep(); // 输出:张教授正在睡觉...// 调用子类特有的方法teacher.teach(); // 输出:张教授正在教授Java编程课程teacher.research(); // 输出:副教授张教授正在进行学术研究// 使用有参构造方法创建对象Teacher mrWang = new Teacher("王老师", 35, "数据结构", "讲师");System.out.println("姓名:" + mrWang.getName() + ",年龄:" + mrWang.getAge() + ",教授科目:" + mrWang.getSubject() + ",职称:" + mrWang.getTitle());mrWang.teach();}
}
在上述代码中, Teacher
类通过 extends
关键字继承了 Person
类,这样 Teacher
类便是 Person
类的子类。
从运行结果可以看出,子类虽然没有直接定义 name
、 age
属性和 eat()
、 sleep()
方法,但是子类却能访问这些属性和方法。这说明,子类在继承父类的时候,会自动拥有父类的属性和方法,并且子类还可以拥有自己特有的属性和方法,即子类可以对父类进行扩展!
3.3 继承的使用要点
继承的优点
- 提高代码复用性 :子类可以直接使用父类的属性和方法,避免了代码的重复编写。
- 提高代码的可维护性 :当父类的代码需要修改时,所有子类都会自动继承这些修改。
- 建立类的层次结构 :通过继承可以建立清晰的类层次结构,使代码更加有序和易于理解。
- 为多态提供基础 :继承是实现多态的前提条件之一。
继承的限制和注意点
- Java只支持单继承,不允许多继承 :一个类只能有一个直接父类。这避免了多继承可能带来的复杂性和歧义(如"菱形继承问题")。
- Java支持多层继承 :一个类的父类可以再去继承另外的父类,形成继承体系。例如:
C extends B
,B extends A
,这样C就间接继承了A。 - 所有类都继承自Object类 :如果定义一个类时没有使用
extends
关键字指定父类,则它的父类默认是java.lang.Object
。Object
类是Java中所有类的根类。 - 子类不能继承父类的构造方法 :构造方法不属于类的成员,不能被继承,但可以通过
super
关键字来调用父类的构造方法。 - 子类不能继承父类的私有成员 :父类中被
private
修饰的成员在子类中不可见,但它们确实存在于子类对象中,只是子类不能直接访问。
3.4 方法的重写(override)
当父类的方法不能满足子类的需求时,我们可以在子类中重写父类的方法。方法重写也称为方法覆盖或方法复写。
方法重写的规则
方法重写需要符合以下规则:
-
方法签名必须相同 :
- 方法名必须完全相同
- 参数列表(参数个数、类型和顺序)必须完全相同
-
访问修饰符限制 :
- 子类方法的访问权限必须大于或等于父类方法的访问权限
- 访问权限从小到大: private < 默认(无修饰符) < protected < public
- 注意:
- 父类中被 private 修饰的方法不能被子类重写
- 父类中被 static 、 final 或 private 修饰的方法不能被子类重写
-
返回值类型限制 :
- 如果父类方法的返回值类型是基本数据类型或 void ,子类重写方法的返回值类型必须相同
- 如果父类方法的返回值类型是引用类型,子类重写方法的返回值类型可以是父类方法返回值类型的子类型
-
异常抛出限制 :
- 子类重写方法抛出的异常类型必须是父类方法抛出异常类型的子类或相同类型
【示例】方法重写案例
// 父类
class Animal {public void makeSound() {System.out.println("动物发出声音");}protected void eat(String food) {System.out.println("动物吃" + food);}public Number getWeight() {return 0;}
}
// 子类
class Dog extends Animal {// 重写父类的makeSound方法@Override // 注解,表明这是一个重写方法public void makeSound() {System.out.println("汪汪汪");}// 重写父类的eat方法,访问权限可以更宽松@Overridepublic void eat(String food) {System.out.println("狗狗吃" + food);}// 重写父类的getWeight方法,返回值类型可以是父类返回值类型的子类@Overridepublic Integer getWeight() { // Integer是Number的子类return 20;}
}
public class OverrideDemo {public static void main(String[] args) {Dog dog = new Dog();dog.makeSound(); // 输出:汪汪汪dog.eat("骨头"); // 输出:狗狗吃骨头System.out.println("狗的体重:" + dog.getWeight() + "kg"); // 输出:狗的体重:20kg// 多态:父类引用指向子类对象Animal animal = new Dog();animal.makeSound(); // 输出:汪汪汪(调用的是子类重写后的方法)}
}
使用super调用父类方法
如果子类方法需要使用父类方法的功能,可以使用 super 关键字来调用父类方法。这样,既能保留父类的功能,又能添加子类特有的功能。
【示例】方法重写应用案例
// 功能手机
class Phone {protected String brand;public Phone(String brand) {this.brand = brand;}public void call() {System.out.println(brand + "手机拨打电话");}public void showNumber() {System.out.println(