Java 面向对象进阶:解锁多态、内部类与包管理 🔑
在 Java 的面向对象编程中,多态赋予了对象“多种形态”的能力,内部类提供了更精细的代码组织方式,而包则帮助我们管理和组织大量的类。今天,我们将深入探讨这些概念,理解它们在实际开发中的应用。
🎭 多态:对象的多种身份
多态性是面向对象编程的三大特性之一(封装、继承、多态)。它允许我们使用一个父类引用来指向子类的对象,从而在运行时根据对象的实际类型执行不同的操作。
-
向上造型 / 自动类型转换 (Upcasting):
- 概念:超类型的引用指向派生类的对象。这是自动发生的,不需要强制转换。
- 规则:能“点”出来什么,看引用的类型。虽然对象实际是子类,但通过父类引用只能访问父类中定义的成员(包括继承自父类的)。子类特有的成员无法直接通过父类引用访问。这是一个重要的规定,请记住它。
- 可以向上造型成为的类型:一个对象可以向上转型成为它所继承的类,以及它所实现的接口。
Animal o2 = new Dog("小黑",2,"黑"); // 狗是动物 - 向上造型 // 通过 o2 只能访问 Animal 类中定义的成员,例如 o2.name, o2.eat() 等 // 无法直接访问 Dog 类特有的 lookHome() 方法 Swim o3 = new Dog("小黑",2,"黑"); // 狗会游泳 - 向上造型 // 通过 o3 只能访问 Swim 接口中定义的方法,例如 o3.swim()
-
向下转型 / 强制类型转换 (Downcasting):
- 概念:将一个超类型的引用强制转换为派生类的引用。
- 成功的条件:强制类型转换成功的条件只有两种:
- 引用实际指向的对象,就是该目标类型。
- 引用实际指向的对象,实现了该目标接口或继承了该目标类。
- 类型转换异常 (ClassCastException):如果强制类型转换不符合上述条件,就会在运行时发生
ClassCastException
异常。 - 建议:在进行向下转型之前,先使用
instanceof
关键字来判断引用所指向的对象是否是目标类型。 instanceof
:返回一个布尔值,表示引用所指向的对象是否是某个类型(类或接口)的实例。instanceof
返回true
的条件,就是强制类型转换成功的条件。
Animal o = new Dog("小黑",2,"黑"); // 向上造型 Dog g = (Dog)o; // 引用 o 所指向的对象就是 Dog 类型,强制转换成功 Swim s = (Swim)o; // 引用 o 所指向的对象实现了 Swim 接口,强制转换成功 // Fish f = (Fish)o; // 运行时会发生 ClassCastException,因为 o 实际指向的是 Dog 对象,而不是 Fish 对象 System.out.println(o instanceof Dog); // true System.out.println(o instanceof Swim); // true System.out.println(o instanceof Fish); // false if(o instanceof Fish){ // 先判断,避免异常 Fish f = (Fish)o; // 可以安全地使用 f 引用访问 Fish 特有的成员 }
- 何时需要强转:当你需要访问对象中在超类中没有定义的,而只在派生类或实现的接口中特有的属性或行为时,就需要进行强制类型转换。
-
多态的实际应用场景:
- 统一处理不同子类对象:将不同子类对象统一封装到超类数组中进行遍历和操作,实现代码复用。例如,将不同类型的动物放入一个
Animal
类型的数组中,然后循环调用它们的eat()
和drink()
方法,即使具体的实现不同(重写),代码结构依然简洁。 - 方法的参数或返回值类型:将超类型(类或接口)作为方法的参数类型或返回值类型,可以使方法接收或返回各种不同的子类对象,扩大方法的应用范围,提高代码的灵活性和复用性。例如,
Master
类中的feed(Animal animal)
方法,可以喂养任何Animal
的子类对象。
// 演示向上造型(多态)的应用: Animal[] animals = new Animal[5]; animals[0] = new Dog("小黑",2,"黑"); // 向上造型 animals[1] = new Dog("小白",1,"白"); animals[2] = new Fish("小金",1,"金"); animals[3] = new Fish("小花",2,"花"); animals[4] = new Chick("小灰",3,"灰"); for(int i=0;i<animals.length;i++){ // 遍历所有动物 System.out.println(animals[i].name); animals[i].drink(); animals[i].eat(); // 多态:根据实际对象调用对应的 eat() 方法 if(animals[i] instanceof Dog){ // 判断是否为 Dog 对象,进行向下转型以访问特有方法 Dog dog = (Dog)animals[i]; dog.lookHome(); } if(animals[i] instanceof Chick){ // 判断是否为 Chick 对象 Chick chick = (Chick)animals[i]; chick.layEggs(); } if(animals[i] instanceof Swim){ // 判断是否实现了 Swim 接口 Swim s = (Swim)animals[i]; s.swim(); // 多态:调用实际对象实现的 swim() 方法 } }
- 统一处理不同子类对象:将不同子类对象统一封装到超类数组中进行遍历和操作,实现代码复用。例如,将不同类型的动物放入一个
🚪 成员内部类:服务于外部类
成员内部类是定义在另一个类内部的非静态类。它通常只服务于其外部类,对外不可见。
- 类中套类:外面的类称为外部类,里面的类称为内部类。
- 可见性:内部类通常只服务于外部类,默认情况下对外不具备可见性。
- 创建对象:内部类对象通常在外部类中创建。
- 访问外部类成员:内部类可以直接访问外部类的所有成员(包括私有的)。在内部类中,有一个隐式的引用指向创建它的外部类对象。
- 这个隐式的引用可以通过
外部类名.this
来访问。 - 例如:
System.out.println(Mama.this.name);
- 这个隐式的引用可以通过
- 何时使用:当一个类 (A 类,例如
Baby
) 只应该被另一个类 (B 类,例如Mama
) 使用,并且 A 类还需要访问 B 类的成员时,可以考虑设计成员内部类。
// 成员内部类示例
public class InnerClassDemo {
public static void main(String[] args) {
Mama m = new Mama();
// Baby b = new Baby(); // 编译错误,内部类对外不具备可见性
}
}
class Mama{ // 外部类
String name = "我是妈妈";
void create(){
Baby b = new Baby(); // 正确,内部类对象通常在外部类中创建
b.show();
}
class Baby{ // 成员内部类
void show(){
System.out.println(name); // 简写,内部类直接访问外部类成员
System.out.println(Mama.this.name); // 完整写法,通过 Mama.this 指代外部类对象
// System.out.println(this.name); // 编译错误,this 在这里指代当前的 Baby 对象,Baby 类没有 name 成员
}
}
}
🤫 匿名内部类:简化一次性使用
匿名内部类是一种特殊的内部类,它没有名字。主要用于创建一个派生类的对象,并且该对象只创建一次。使用匿名内部类可以大大简化代码。
- 何时使用:当你需要创建一个接口的实现类或一个类的子类的对象,并且只需要创建一次这个对象时,可以使用匿名内部类。
- 语法:
接口名/类名 对象名 = new 接口名/类名() { // 匿名内部类的类体 // 实现接口中的抽象方法 或 重写父类方法 // 可以有自己的成员,但通常不常见 };
- 注意:匿名内部类中不能修改外部局部变量的值。这是因为编译器会默认将匿名内部类中访问到的外部局部变量变为
final
的。 - 小面试题:内部类有独立的
.class
文件吗?- 答:有。编译器会为匿名内部类生成一个带有特殊命名规则的
.class
文件。
- 答:有。编译器会为匿名内部类生成一个带有特殊命名规则的
// 匿名内部类示例
public class AnonInnerClassDemo {
public static void main(String[] args) {
// 使用匿名内部类实现 InterInter 接口
InterInter o3 = new InterInter(){
public void show(){ // 实现 InterInter 接口中的抽象方法 show()
System.out.println("showshow");
// num = 6; // 编译错误,不能修改外部局部变量 num
}
};
o3.show(); // 调用匿名内部类中实现的 show() 方法
int num = 5;
// num = 6; // 这里修改 num 是合法的
}
}
interface InterInter{
void show();
}
📦 package 与 import:组织和引用代码
随着项目规模的增大,类会越来越多。package
和 import
关键字帮助我们组织和管理这些类,避免命名冲突。
-
package:声明包
- 作用:将相关的类组织在一起,形成一个“包”,从而避免类的命名冲突。
- 规定:同一个包中的类不能同名,但不同包中的类可以同名。
- 类的全称:一个类的完整名称包括包名和类名,格式为
包名.类名
。包名常常具有层次结构,用点 (.
) 分隔。 - 建议:包名所有字母都小写。
package
语句必须是 Java 源文件的第一条非注释语句。
-
import:导入类
- 当一个类需要使用不同包中的其他类时,不能直接访问,需要先导入。
- 方式 1 (建议):使用
import
关键字导入需要使用的类。import java.util.Scanner; // 导入 java.util 包下的 Scanner 类 // 现在可以直接使用 Scanner 类名 Scanner scan = new Scanner(System.in);
- 方式 2 (不建议):使用类的全称来访问。这种方式太繁琐,不建议在代码中大量使用。
java.util.Scanner scan = new java.util.Scanner(System.in); // 使用类的全称
- 导入同一个包下的多个类:可以使用通配符
*
来导入包下的所有类,例如import java.util.*;
。但这通常不如明确导入所需的类那样清晰。 import
语句必须放在package
语句之后,所有类定义之前。
补充:
- 隐式的引用总结:
this
:指代当前对象。super
:指代当前对象的超类对象。外部类名.this
:指代当前内部类对象所依赖的外部类对象。
- 关键字顺序:在一个 Java 源文件中,关键字的顺序通常是:先
package
,再import
,最后class
定义。
多态、内部类和包管理是 Java 中非常实用的特性。理解并熟练运用它们,能够帮助我们编写出更加灵活、可维护和结构清晰的代码。继续练习,你会越来越得心应手!
总结
- 多态:多种形态
- 向上造型:超类型的引用指向派生类的对象
- 向下转型,成功的条件:
2.1) 引用所指向的对象,就是该类型
2.2) 引用所指向的对象,实现了该接口或继承了该类
- 匿名内部类:
- 若想创建一个派生类的对象,并且对象只创建一次,可以设计为匿名内部类
可以大大简化代码
- 若想创建一个派生类的对象,并且对象只创建一次,可以设计为匿名内部类
- package 用于声明包,import 用于导入类
提示
- 什么是多态
- 多态的应用场景有哪些
- 匿名内部类的应用场景有哪些
- package 和 import 的作用
- 不同包中的类如何访问