当前位置:首页分享Java从入门到放弃(二十四):继承(下)

Java从入门到放弃(二十四):继承(下)

阻止继承

正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。

从Java 15开始,引入了一个新的修饰符sealed,用于限制某个类的继承。当一个类被声明为sealed时,它只允许特定的类继承它,这些特定的类需要在permits子句中明确列出。

例如,考虑以下Shape类的定义:

public sealed class Shape permits Rect, Circle, Triangle {  
    // ... 类的其他部分  
}

在这个例子中,Shape类是一个sealed类,它只允许RectCircleTriangle这三个类继承它。如果尝试如下定义一个Rect类:

public final class Rect extends Shape {  
    // ... 类的其他部分  
}

这是没有问题的,因为Rect确实出现在Shape类的permits列表中。然而,如果尝试定义一个不在列表中的Ellipse类:

public final class Ellipse extends Shape {  
    // ... 类的其他部分  
}

这将会导致编译错误,错误信息类似于:

Compile error: class is not allowed to extend sealed class: Shape

这是因为Ellipse类并未出现在Shape类的permits列表中。这种sealed类的设计主要是用于一些框架中,以限制继承的滥用,保证继承体系的清晰和可控。通过限制哪些类可以继承某个类,可以更好地管理类的继承关系,减少因继承带来的潜在问题和复杂性。

向上转型

在Java等面向对象的编程语言中,类继承是实现代码复用和层次化设计的重要手段。当一个类(如Student)继承自另一个类(如Person)时,子类会继承父类的所有属性和方法。这种继承关系允许我们创建更具体的对象类型,而这些类型可以看作是更一般类型(父类)的特例。

在Java中,变量的类型决定了它可以引用哪些类型的对象。如果一个变量的类型是Student,那么它只能引用Student类型的实例,如:

Student s = new Student();

同样地,如果变量的类型是Person,它通常用于引用Person类型的实例:

Person p = new Person();

然而,当Student类是从Person类继承下来时,情况就有所不同了。由于StudentPerson的一个子类,因此Student类型的实例也是Person类型的一个实例。这意味着一个类型为Person的变量可以安全地引用一个Student类型的实例,即:

Person p = new Student(); // 这是允许的

这种将子类对象赋值给父类类型变量的操作,被称为向上转型(upcasting)。向上转型是安全的,因为子类对象确实包含了父类对象的所有属性和方法。在向上转型后,通过父类类型的变量,我们可以访问子类对象继承自父类的所有属性和方法。

由于继承关系形成了一个层次结构,例如Student > Person > Object,我们可以进行多级的向上转型。例如:

Student s = new Student();  
Person p = s; // upcasting from Student to Person  
Object o1 = p; // further upcasting from Person to Object  
Object o2 = s; // direct upcasting from Student to Object

向下转型

向下转型是面向对象编程中另一种类型转换的方式,它与向上转型相反。向上转型是安全的,因为子类对象确实包含了父类对象的所有属性和方法。而向下转型则是将父类类型的引用转换为子类类型的引用,这通常是不安全的,因为父类引用可能并不指向子类对象。

例如,假设我们有一个Person类和一个继承自PersonStudent类。如果我们有一个Person类型的引用指向了一个Student对象,那么我们可以安全地将其向上转型为Person类型。但如果我们有一个Person类型的引用指向了一个普通的Person对象,却尝试将其向下转型为Student类型,那么就会抛出ClassCastException异常。

Person p1 = new Student(); // 向上转型,安全  
Person p2 = new Person();   // 创建一个普通的Person对象  
  
// 尝试向下转型  
Student s1 = (Student) p1; // 成功,因为p1确实指向Student对象  
// Student s2 = (Student) p2; // 这行会抛出ClassCastException

为了避免向下转型时可能出现的错误,我们应该先使用instanceof操作符来检查引用是否确实指向了目标子类的实例。

if (p1 instanceof Student) {  
    Student s1 = (Student) p1; // 安全向下转型  
    // 现在可以使用s1访问Student特有的方法  
}  
  
if (p2 instanceof Student) {  
    Student s2 = (Student) p2;  
    // 这部分代码不会执行,因为p2不是Student的实例  
} else {  
    System.out.println("p2 is not a Student.");  
}

从Java 14开始,我们可以利用模式匹配(Pattern Matching)来简化这个过程,直接在instanceof检查中声明变量,避免后续的显式强制类型转换。

if (p1 instanceof Student s3) {  
    // 直接使用s3,它已经是Student类型了  
    // 无需再进行(Student)强制类型转换  
    s3.studentSpecificMethod(); // 假设Student类有一个特有的方法  
}  
  
// 对于p2,因为不是Student的实例,所以下面的代码块不会执行  
if (p2 instanceof Student s4) {  
    // 这部分代码不会执行  
}

这种模式匹配的特性不仅减少了代码中的样板代码,还提高了代码的可读性和安全性。通过直接在instanceof中声明变量,我们可以确保只有在类型匹配成功的情况下才会执行后续的代码块,并且可以直接使用新声明的变量,无需担心类型转换的问题。

区分继承和组合

当我们考虑使用继承时,必须确保逻辑上的一致性。让我们来看一个关于Book类的例子:

class Book {  
    protected String name;  
    public String getName() {  
        // ... 实现细节  
    }  
    public void setName(String name) {  
        // ... 实现细节  
    }  
}

在这个Book类中,有一个字段name表示书名,并提供了获取和设置书名的方法。

现在,假设我们考虑将Student类设计为继承自Book类:

class Student extends Book {  
    protected int score;  
}

从逻辑上分析,这样的设计是不合理的。Student不应该是Book的子类,因为学生并不是书的一种类型。在现实中,学生和书之间的关系并不是“is-a”关系,即学生不是书。实际上,学生拥有书,这是一个“has-a”关系。

因此,当我们面对“has”关系时,不应该使用继承,而是应该使用组合。组合意味着一个类可以包含另一个类的对象作为其成员。在这种情况下,一个Student类应该持有一个Book类的实例:

class Student extends Person {  
    protected Book book;  
    protected int score;  
      
    // ... 其他方法  
}

在这个设计中,Student类继承自Person类,表示学生是人的一种。同时,学生类包含一个Book类型的成员变量book,表示学生拥有一本书。这样的设计既符合逻辑上的“is-a”关系(学生是人),又合理地表达了“has-a”关系(学生有一本书)。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧