Java中的多态性

1. 概述

所有面向对象编程 (OOP) 语言都需要表现出四个基本特征:抽象、封装、继承和多态性。

在本文中,我们介绍了两种核心类型的多态性:静态或编译时多态性以及动态或运行时多态性。静态多态性在编译时强制执行,而动态多态性在运行时实现。

2. 静态多态性

根据维基百科,静态多态性是对多态性的模仿,在编译时解析,消除了运行时虚拟表查找。

例如,文件管理器应用程序中的 TextFile 类有三个同名不同签名的方法:

public class TextFile extends GenericFile {
    //...

    public String read() {
        return this.getContent()
          .toString();
    }

    public String read(int limit) {
        return this.getContent()
          .toString()
          .substring(0, limit);
    }

    public String read(int start, int stop) {
        return this.getContent()
          .toString()
          .substring(start, stop);
    }
}

在代码编译期间,编译器验证 read 方法的所有调用是否至少对应于上面定义的三种方法之一。

3. 动态多态性

通过动态多态性,Java 虚拟机 (JVM) 处理在将子类分配给其父类时要执行的相应方法的检测。这是必需的,因为子类可能会重写父类中定义的部分或全部方法。

在一个假设的文件管理器应用中,让我们先在父类 GenericFile中 定义一个方法getFileInfo:

public class GenericFile {
    private String name;

    //...

    public String getFileInfo() {
        return "Generic File Impl";
    }
}

接我们实现一个 ImageFile 类,它扩展了 GenericFile,但它覆盖了 getFileInfo() 方法并附加了更多信息:

public class ImageFile extends GenericFile {
    private int height;
    private int width;

    //... getters and setters
    
    public String getFileInfo() {
        return "Image File Impl";
    }
}

当我们创建 ImageFile 的实例并将其分配给 GenericFile 类时,将完成隐式强制转换。但是,JVM保留对ImageFile实际形式的引用。

上述构造类似于方法重写。我们可以通过调用 getFileInfo() 方法来确认这一点:

public static void main(String[] args) {
    GenericFile genericFile = new ImageFile("SampleImageFile", 200, 100, 
      new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB)
      .toString()
      .getBytes(), "v1.0.0");
    logger.info("File Info: \n" + genericFile.getFileInfo());
}

正如预期的那样,genericFile.getFileInfo() 触发了 ImageFile 类的 getFileInfo() 方法,如下面的输出所示:

File Info: 
Image File Impl

4. Java中的其他多态特征

除了Java中的这两种主要多态性类型之外,Java编程语言中还有其他特征表现出多态性。让我们讨论其中的一些特征。

4.1. 强制

多态强制处理编译器完成的隐式类型转换,以防止类型错误。一个典型的例子是整数和字符串连接:

String str = “string” + 2;

4.2. 运算符重载

运算符或方法重载是指同一符号或运算符的多态特征,根据上下文具有不同的含义(形式)。

例如,加号 (+) 可用于数学加法以及字符串串联。在任何一种情况下,只有上下文(即参数类型)确定符号的解释:

String str = "2" + 2;
int sum = 2 + 2;
System.out.printf(" str = %s\n sum = %d\n", str, sum);

输出:

str = 22
sum = 4

4.3. 多态参数

参数化多态性允许类中的参数或方法的名称与不同的类型相关联。我们在下面有一个典型的例子,我们先将内容定义为字符串,后来又定义为整型

public class TextFile extends GenericFile {
    private String content;
    
    public String setContentDelimiter() {
        int content = 100;
        this.content = this.content + content;
    }
}

同样重要的是要注意,多态参数的声明可能会导致称为变量隐藏的问题,其中参数的本地声明始终覆盖具有相同名称的另一个参数的全局声明。

要解决此问题,通常建议使用全局引用(如 this 关键字)来指向局部上下文中的全局变量。

4.4. 子类型多态

多态子类型方便地使我们能够为一个类型分配多个子类型,并期望对该类型的所有调用都触发子类型中的可用定义。

例如,如果我们有一个 GenericFiles 的集合,并且我们对每个集合调用 getInfo() 方法,我们可以预期输出会有所不同,具体取决于集合中每个项派生的子类型:

GenericFile [] files = {new ImageFile("SampleImageFile", 200, 100, 
  new BufferedImage(100, 200, BufferedImage.TYPE_INT_RGB).toString() 
  .getBytes(), "v1.0.0"), new TextFile("SampleTextFile", 
  "This is a sample text content", "v1.0.0")};
 
for (int i = 0; i < files.length; i++) {
    files[i].getInfo();
}

子类型多态性可以通过向上转换和晚绑定的组合实现。向上转换是指将继承层次从超类型转换为子类型::

ImageFile imageFile = new ImageFile();
GenericFile file = imageFile;

上述代码的结果是,无法在新的向上转换GenericFile上调用特定于imagefile的方法。不过,子类型中的方法会覆盖超类型中定义的类似方法。。

为了解决在向上转换为超类型时无法调用特定于子类型的方法的问题,我们可以对从超类型到子类型的继承进行向下转换。这是通过以下方式完成的:

ImageFile imageFile = (ImageFile) file;

后期绑定策略可帮助编译器解析在向上转换后触发谁的方法。在上面的例子中,imageFile#getInfo vs file#getInfo,编译器保留对ImageFilegetInfo方法的引用。

5. 多态性问题

让我们看一下多态性中的一些歧义,如果未正确检查,可能会导致运行时错误。

5.1. 向下转换过程中的类型识别问题

回想一下,我们之前在执行上转换后无法访问某些特定于子类型的方法。尽管我们能够通过向下的转换来解决此问题,但这并不能保证实际的类型检查。

例如,如果我们执行上转和随后的下转:

GenericFile file = new GenericFile();
ImageFile imageFile = (ImageFile) file;
System.out.println(imageFile.getHeight());

我们注意到编译器允许将 GenericFile 向下转换为 ImageFile,即使该类实际上是 GenericFile 而不是 ImageFile

因此,如果我们尝试在imageFile类上调用getHeight()方法,我们会得到一个ClassCastException,因为GenericFile没有定义getHeight()方法:

Exception in thread "main" java.lang.ClassCastException:
GenericFile cannot be cast to ImageFile

为了解决此问题,JVM 执行运行时类型信息 (RTTI) 检查。我们还可以使用 instanceof 关键字尝试显式类型标识,如下所示:

ImageFile imageFile;
if (file instanceof ImageFile) {
    imageFile = file;
}

上述有助于避免运行时出现 ClassCastException 异常。另一个可以使用的选项是将强制转换包装在 try 和 catch 块中并捕获 ClassCastException。

应该注意的是,RTTI 检查是昂贵的,因为有效验证类型是否正确所需的时间和资源。此外,频繁使用实例关键字几乎总是意味着糟糕的设计。

5.2. 脆弱基类问题

根据维基百科,如果对基类看似安全的修改可能导致派生类出现故障,则基类或超类被认为是脆弱的。

让我们考虑一个名为 GenericFile 的超类及其子类 TextFile 的声明:

public class GenericFile {
    private String content;

    void writeContent(String content) {
        this.content = content;
    }
    void toString(String str) {
        str.toString();
    }
}
public class TextFile extends GenericFile {
    @Override
    void writeContent(String content) {
        toString(content);
    }
}

当我们修改 GenericFile 类时:

public class GenericFile {
    //...

    void toString(String str) {
        writeContent(str);
    }
}

我们观察到,上述修改使 TextFile 在 writeContent() 方法中处于无限递归状态,最终导致堆栈溢出。

为了解决脆弱的基类问题,我们可以使用 final 关键字来防止子类覆盖 writeContent() 方法。适当的文档也可以提供帮助。最后我的建议是使用组合来解决这类继承带来的问题。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值