1. 概述

所有面向对象编程(OOP)语言都必须展现四个基本特性:抽象、封装、继承和多态。本文聚焦于多态性,重点探讨两种核心类型:静态多态(编译时多态)动态多态(运行时多态)。静态多态在编译时确定,而动态多态则在运行时解析。

2. 静态多态

静态多态本质是编译时解析的多态模拟,它消除了运行时的虚表查找开销。以文件管理器中的TextFile类为例,它包含三个同名read()方法:

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. 动态多态

动态多态由JVM处理:当子类被赋值给父类引用时,JVM会动态选择要执行的方法版本。这是因为子类可能重写了父类的方法。

假设文件管理器中定义了所有文件的父类GenericFile

public class GenericFile {
    private String name;

    //...

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

再创建子类ImageFile,它重写了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类型的引用:

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());
}

输出结果证实了方法重写机制:

File Info: 
Image File Impl

4. 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. 参数多态

参数多态允许类中的参数或方法名关联不同类型。以下示例中content先被定义为String,后又被定义为Integer

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

⚠️ 注意:这种声明可能导致变量隐藏问题——局部变量会覆盖同名全局变量。解决方案是使用this关键字明确引用全局变量。

4.4. 多态子类型

多态子类型允许将多个子类型赋值给父类型引用,调用方法时触发子类的具体实现。例如处理GenericFile集合时:

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;
    
    ❌ 转型后无法调用子类特有方法
  • 向下转型:将父类引用转回子类
    ImageFile imageFile = (ImageFile) file;
    
  • 延迟绑定:编译器通过保留子类方法引用来解析调用

5. 多态带来的问题

5.1. 向下转型时的类型识别

向上转型后无法调用子类方法,虽然可通过向下转型解决,但存在类型安全风险:

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

编译器允许这种转型,但运行时会抛出ClassCastException

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

解决方案:

  1. 使用instanceof检查类型:
    ImageFile imageFile;
    if (file instanceof ImageFile) {
        imageFile = file;
    }
    
  2. try-catch捕获异常

⚠️ 注意:RTTI(运行时类型信息)检查开销较大,频繁使用instanceof通常意味着设计缺陷。

5.2. 脆弱基类问题

当对基类的"安全"修改导致子类失效时,就出现了脆弱基类问题。考虑以下基类和子类:

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);
    }
}

如果修改基类toString方法:

public class GenericFile {
    //...

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

会导致TextFilewriteContent()陷入无限递归,最终栈溢出。

解决方案:

  • final阻止方法重写
  • 完善文档说明
  • 优先使用组合而非继承

6. 总结

本文深入探讨了Java多态性的核心概念及其优缺点。多态是OOP的基石,但需谨慎处理类型转换和继承设计以避免踩坑。完整代码示例可在GitHub获取。


原始标题:Polymorphism in Java | Baeldung

« 上一篇: Java HashSet 完全指南
» 下一篇: Java每周,问题207