Java包装类:你需要掌握的核心要点

自Java 21起,包装类在Java类型系统中扮演着日益复杂的角色。以下是关于虚拟线程、模式匹配等方面更新所需了解的全部信息。

你是否曾好奇Java如何无缝地将其基本数据类型与面向对象编程相结合?这就引入了包装类——一个重要但常被忽视的Java特性。这些特殊类在基本类型(如intdouble)与对象之间架起了桥梁,使您能够在集合中存储数字、处理空值、使用泛型,甚至在现代特性(如模式匹配)中处理数据。
无论您是在使用List<Integer>还是从字符串解析Double,Java的包装类都使其成为可能。在本文中,我们将介绍Java 21(当前Java的长期支持版本)中的包装类。我还会提供在使用包装类时的技巧、示例以及equals()hashCode()中需要避免的陷阱。

在深入探讨Java 21中包装类的新特性之前,我们先快速回顾一下。

包装类的定义和用途

Java包装类是最终(final)、不可变的类,它们将基本值“包装”在对象内部。每种基本类型都有一个对应的包装类:

  • Boolean
  • Byte
  • Character
  • Short
  • Integer
  • Long
  • Float
  • Double

这些包装类有多种用途:

  • 允许在需要对象的地方使用基本类型(例如,在集合和泛型中)。
  • 提供用于类型转换和操作的实用方法。
  • 支持空值(null),而基本类型不能。
  • 支持反射和其他面向对象的操作。
  • 通过对象方法实现一致的数据处理。

Java版本中包装类的演变

在整个Java历史中,包装类经历了显著的演变:

  • Java 1.0 到 Java 1.4:引入了基本的包装类,并需要手动装箱和拆箱。
  • Java 5:增加了自动装箱和拆箱,极大地简化了代码。
  • Java 8:通过新的实用方法和函数式接口兼容性增强了包装类。
  • Java 9:弃用了包装类构造函数,推荐使用工厂方法。
  • Java 16 到 17:加强了弃用警告,并为移除包装类构造函数做准备。
  • Java 21:改进了包装类的模式匹配,并进一步优化了其在虚拟线程中的性能。

这种演变反映了Java在向后兼容性和集成现代编程范式之间持续的平衡。

Java 21类型系统中的包装类

从Java 21开始,包装类在Java类型系统中扮演着日益复杂的角色:

  • 增强的switchinstanceof模式匹配与包装类型无缝协作。
  • 与记录模式(record patterns)自然集成,实现更清晰的数据操作。
  • 优化了包装类型与虚拟线程系统之间的交互。
  • 改进了Lambda表达式和方法引用中包装类的类型推断。

Java 21中的包装类在承担其基本桥梁作用的同时,也融合了现代语言特性,使其成为当代Java开发的重要组成部分。

Java 21中的基本数据类型和包装类

Java为每种基本类型提供了一个包装类,为语言的基本值创建了完整的面向对象表示。以下是基本类型及其对应包装类的快速回顾,并附有创建示例:

基本类型 (Primitive type) 包装类 (Wrapper class) 创建示例 (Example creation)
boolean java.lang.Boolean Boolean.valueOf(true)
byte java.lang.Byte Byte.valueOf((byte)1)
char java.lang.Character Character.valueOf('A')
short java.lang.Short Short.valueOf((short)100)
int java.lang.Integer Integer.valueOf(42)
long java.lang.Long Long.valueOf(10000L)
float java.lang.Float Float.valueOf(3.14F)
double java.lang.Double Double.valueOf(2.71828D)

每个包装类都扩展了Object并实现了ComparableSerializable等接口。包装类提供了超越其基本对应物的附加功能,例如能够使用equals()方法进行比较。

包装类方法

Java的包装类提供了一组丰富的实用方法,超越了其装箱基本类型的主要角色。这些方法提供了方便的方式来解析字符串、转换类型、执行数学运算和处理特殊值。

类型转换方法

  • 字符串解析:Integer.parseInt("42"), Double.parseDouble("3.14")
  • 跨类型转换:intValue.byteValue(), intValue.doubleValue()
  • 进制转换:Integer.parseInt("2A", 16), Integer.toString(42, 2)
  • 无符号操作:Integer.toUnsignedLong()

实用方法

  • 最小/最大值函数:Integer.min(a, b), Long.max(x, y)
  • 比较:Double.compare(d1, d2)
  • 数学运算:Integer.sum(a, b), Integer.divideUnsigned(a, b)
  • 位操作:Integer.bitCount(), Integer.reverse()
  • 特殊值检查:Double.isNaN(), Float.isFinite()

valueOf()

另一个需要了解的重要方法是valueOf()。构造函数在Java 9中被弃用,并在Java 16中标记为待移除。不用构造函数的一种方法是改用工厂方法;例如,使用Integer.valueOf(42)而不是new Integer(42)valueOf()的优点包括:

  • 对基本类型包装器进行内存高效的缓存(IntegerShortLongByte缓存-128到127;Character缓存0-127;Boolean缓存TRUE/FALSE常量)。
  • FloatDouble由于其浮点值范围而不进行缓存。
  • 一些工厂方法对空输入有明确的行为定义。

模式匹配和虚拟线程的包装类更新

Java 21中的包装类针对模式匹配和虚拟线程进行了优化。Java中的模式匹配允许您测试对象的结构和类型,同时提取其组件。Java 21显著增强了switch语句的模式匹配,特别是在包装类方面。如下例所示,增强的模式匹配在处理多态数据时能够实现更简洁和类型安全的代码:

public String describeNumber(Number n) {
    return switch (n) {
        case Integer i when i < 0 -> "Negative integer: " + i;
        case Integer i -> "Positive integer: " + i;
        case Double d when d.isNaN() -> "Not a number";
        case Double d -> "Double value: " + d;
        case Long l -> "Long value: " + l;
        case null -> "No value provided";
        default -> "Other number type: " + n.getClass().getSimpleName();
    };
}

模式匹配的主要改进包括:

  • 空值处理(
    Null handling)
    :显式的空值case防止了意外的NullPointerException
  • 守卫模式(Guard patterns):when子句支持复杂的条件匹配。
  • 类型细化(Type refinement): 编译器现在理解每个case分支内的细化类型。
  • 嵌套模式(Nested patterns): 模式匹配现在支持涉及嵌套包装对象的复杂模式。
  • 穷举性检查(Exhaustiveness checking): 您现在可以获得编译器验证,确保覆盖了所有可能的类型。

这些特性使得包装类的处理更加类型安全和富有表现力,特别是在处理混合了基本类型和对象数据的代码中。

Java 21的虚拟线程特性也与包装类有几个重要的交互方式。首先,在并发上下文中的装箱开销减少了,如下所示:

// 使用虚拟线程高效处理大量数字流
void processNumbers(List<Integer> numbers) {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        numbers.forEach(num -> 
            executor.submit(() -> processNumber(num))
        );
    }
}

虚拟线程的其他更新包括:

  • JVM优化了涉及包装类的线程通信,减少了虚拟线程调度和交接的开销。
  • 线程本地缓存也得到了改进。包装类缓存(Integer等类型的-128到127)是按载体线程(carrier thread)而不是按虚拟线程维护的,防止了高并发场景下不必要的内存使用。
  • 还添加了身份保留(Identity preservation)。在单个虚拟线程内,包装类的身份被适当地维护,以用于同步和身份敏感的操作。

最后,对包装类进行了优化,以提高其在虚拟线程中的性能:

  • 虚拟线程使用栈遍历(stack walking)进行各种操作。包装类优化了这些交互。
  • 虚拟线程调度器队列中的包装类受益于内存效率的改进。
  • 通过优化的拆箱操作减少了线程固定(thread pinning)的风险。
  • 结构化并发模式与包装类值组合无缝协作。

包装类和虚拟线程之间的集成确保了包装类在Java 21引入的新并发编程模型中保持其有用性。这里描述的变化确保了包装类在Java中继续发挥作用,而不会出现在高吞吐量、虚拟线程密集型应用中可能发生的性能损失。

包装类中的Equals和hashcode实现

包装类重写了equals()方法,以执行基于值的比较,而不是Object.equals()使用的引用比较。在基于值的比较中,两个包装对象如果包含相同的基本值,则它们是相等的,无论它们是否是内存中不同的对象。这种比较类型具有类型特定性和空值安全性的优点:

  • 类型特异性: 仅当两个对象都是完全相同的包装类型时,比较才返回true。
  • 空值安全性: 所有包装类实现都能安全地处理空值比较。

在下面的例子中,Integer.equals()检查参数是否为Integer并且具有相同的int值:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

需要注意几个特殊情况:

  • FloatDouble 这些包装类一致地处理像NaN这样的特殊值。(与基本类型比较不同,在equals()NaN等于NaN。)
  • 自动装箱: 当使用==而不是equals()进行比较时,由于对某些值的缓存,自动装箱可能导致意外行为。

基于哈希的集合中的包装类

包装类以实现与其基本值直接对应的hashCode()方式,确保了在基于哈希的集合中的一致行为。这对于HashMapHashSetConcurrentHashMap等集合至关重要。考虑以下实现细节,然后我们将看几个例子。

  • IntegerShortByte:直接将基本值作为哈希码返回。
  • Long:将低32位与高32位进行异或操作:((int)(value ^ (value >>> 32)))
  • Float:使用Float.floatToIntBits()转换为原始位,以处理像NaN这样的特殊值。
  • Double:转换为原始位,然后对结果位使用Long的策略。
  • Character:将Unicode代码点作为哈希码返回。
  • Boolean:返回1231表示true1237表示false(任意但一致的值)。

在基于哈希的集合中使用包装类有几个优点:

  • 性能: 基于哈希的集合依赖分布良好的哈希码来实现O(1)的查找性能。
  • 一致性: hashCode()约定要求相等的对象产生相等的哈希码,包装类保证了这一点。
  • 特殊值处理: 正确处理边缘情况,如浮点类型中的NaN(两个NaN值在哈希码中是相等的,尽管使用equals()比较时不相等)。
  • 分布: 实现旨在最小化常见值模式的哈希冲突。
  • 不可变性: 由于包装对象是不可变的,它们的哈希码可以在首次计算后安全地缓存,从而提高性能。

这种谨慎的实现确保了包装类能够可靠地作为基于哈希的集合中的键,这是Java应用程序中的常见用例。

== 与 .equals() 包装类陷阱

我见过许多由使用==而不是.equals()比较包装对象引起的错误。这是一个经典的Java陷阱,甚至困扰着有经验的开发人员。你可以从这里看到是什么让它如此棘手:

Integer a = 100;
Integer b = 100;
System.out.println(a == b);      // 输出: true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);      // 输出: false (等等,什么?)

这种令人困惑的行为发生是因为Java在内部缓存了常用值的Integer对象(通常是-128到127)。在这个范围内,Java重用相同的对象,而在缓存范围之外,你会得到新的对象。

这就是为什么黄金法则很简单:在比较包装对象时,始终使用.equals()。这个方法始终检查值相等性(value equality)而不是对象同一性(object identity):

// 无论缓存如何,这种方法都能可靠地工作
if (wrapperA.equals(wrapperB)) {
    // 值相等
}

空值拆箱陷阱

开发人员花费大量时间试图理解令人困惑的NullPointerException的起源,如下所示:

Integer wrapper = null;
int primitive = wrapper; // 运行时抛出 NullPointerException

这段看似无害的代码编译时没有警告,但在运行时崩溃。当Java尝试将空(null)包装器拆箱为其基本等效值时,它会尝试在空引用上调用intValue()等方法,从而导致NullPointerException

这个问题特别危险,因为它静默地通过编译,错误只在执行期间出现,并且通常发生在方法参数、数据库结果和集合处理中。为了保护你的代码,你可以使用以下防御策略:

  • 显式空值检查;例如,int primitive = (wrapper != null) ? wrapper : 0;
  • Java 21 模式匹配;例如,int value = (wrapper instanceof Integer i) ? i : 0;
  • 提供默认值;例如,int safe = Optional.ofNullable(wrapper).orElse(0);

在包装对象和基本类型之间转换时,尤其是在处理可能包含来自外部源或数据库查询的空值的数据时,务必小心。

包装类常量(不要重复造轮子)

每个Java开发人员可能都曾在某个时候写过类似“if (temperature > 100)”的代码。但是当你需要检查一个值是否超过整数的最大容量时呢?硬编码2147483647是滋生bug的温床。

相反,你可以使用带有内置常量的包装类:

// 这样清晰且自文档化
if (calculatedValue > Integer.MAX_VALUE) {
    logger.warn("Value overflow detected!");
}

最有用的常量分为两类。
数值限制有助于防止溢出错误:

  • Integer.MAX_VALUEInteger.MIN_VALUE
  • 需要更大范围时使用 Long.MAX_VALUE

浮点特殊值处理边缘情况:

  • Double.NaN 表示“非数字”结果。
  • 需要表示时使用 Double.POSITIVE_INFINITY

我发现这些在处理金融计算或处理科学数据(其中特殊值很常见)时特别有用。

包装类的内存和性能影响

理解包装类的内存和性能影响至关重要。首先,每个包装对象需要16字节的头部开销:12字节用于对象头部,4字节用于对象引用。我们还必须考虑实际的基本值存储(例如,Integer为4字节,Long为8字节等)。最后,集合中的对象引用增加了另一层内存使用,在大型集合中使用包装对象也比使用基本类型数组显著增加内存。

还有性能方面的考虑。首先,尽管有JIT优化,但在紧密循环中重复装箱和拆箱会影响性能。另一方面,像Integer这样的包装类缓存常用值(默认-128到127),减少了对象创建。此外,现代JVM有时可以在包装对象不“逃逸”方法边界时完全消除其分配。Valhalla项目旨在通过引入专门的泛型和值对象来解决这些低效问题。

考虑以下减少包装类性能和内存影响的最佳实践指南:

  • 对性能关键代码和大型数据结构使用基本类型。
  • 在需要对象行为时(例如,集合和可空性)利用包装类。
  • 考虑使用像Eclipse Collections这样的专门库来处理大量的“包装”基本类型集合。
  • 注意对包装对象进行身份比较(==)。
  • 始终使用Objectequals()方法来比较包装器。
  • 在优化之前进行分析,因为JVM对包装器的行为在不断改进。

虽然包装类与基本类型相比会产生开销,但Java的持续发展在保持面向对象范式优势的同时,正在不断缩小这一差距。

包装类的一般最佳实践

理解何时使用基本类型 versus 包装类对于编写高效且可维护的Java代码至关重要。虽然基本类型提供更好的性能,但包装类在某些场景下提供了灵活性,例如处理空值或使用Java的泛型类型。通常,您可以遵循以下准则:

在以下情况下使用基本类型:

  • 局部变量
  • 循环计数器和索引
  • 性能关键代码
  • 返回值(当null没有意义时)

在以下情况下使用包装类:

  • 可以为空的类字段
  • 泛型集合(例如,List<Integer>
  • 返回值(当null具有含义时)
  • 泛型中的类型参数
  • 使用反射时

结论

Java包装类是基本类型与Java面向对象生态系统之间的重要桥梁。从它们在Java 1.0中的起源到Java 21中的增强,这些不可变类使基本类型能够参与集合和泛型,同时提供丰富的转换和计算实用方法。它们谨慎的实现确保了在基于哈希的集合中的一致行为,并提供了提高代码正确性的重要常量。

虽然包装类与基本类型相比会产生一些内存开销,但现代JVM通过缓存和JIT编译优化了它们的使用。最佳实践包括使用工厂方法代替已弃用的构造函数,使用.equals()进行值比较,以及为性能关键代码选择基本类型。随着Java 21模式匹配改进和虚拟线程集成,包装类在保持向后兼容性的同时继续发展,巩固了它们在Java开发中的重要性。

Spring框架中的Component与Bean注解

Spring Boot 中的 @Bean 与 @Component

Spring 的 @Component@Bean 注解的关键区别在于:@Bean 注解可用于暴露您自己编写的 JavaBeans,而 @Component 注解可用于暴露源代码由他人维护的 JavaBeans。
Spring 框架的核心是其控制反转 (IoC) 容器,它管理着应用程序中最重要的 JavaBeans 的生命周期。然而,IoC 容器并不管理应用程序可能需要的每一个 JavaBean。它只管理您明确要求它管理的 JavaBeans 的生命周期。
何时使用 Spring 的 @Bean 注解?
如果您自己编写了一个 JavaBean,可以直接在源代码中添加 Spring 的 @Bean 注解。这里我们要求 Spring 的 IoC 容器管理 Score 类所有实例的生命周期。

@Bean
public class Score {
    int wins, losses, ties;
}

何时使用 Spring 的 @Component 注解?
但是,如果您想让 Spring 的 IoC 容器管理来自 Jackson API 的 ObjectMapper,或者来自 JDBC API 的 DataSource 组件呢?您不能简单地编辑 JDK 中的代码并在标准 API 的类上添加 @Bean 注解。这就是 @Component 注解的用武之地。
如果您希望 Spring 管理一个您无法控制其代码的 JavaBean,您可以创建一个返回该 JavaBean 实例的方法,然后用 @Component 注解装饰该方法,如下例所示:

@Configuration
public class MyConfig {
    @Component
    public DataSource getMyHikariDataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:h2:mem:roshambo");
        return ds;
    }
    @Component
    public ObjectMapper getMyObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        return mapper;
    }
}

在此示例中,我们使用了 @Component 注解来告诉 Spring IoC 容器管理 DataSourceObjectMapper bean 的生命周期。
这些组件来自 Jackson 和 JDBC API,因此我们无法编辑其源代码。这就是为什么我们不能直接在类声明上方添加 @Bean 注解的原因。但是,我们可以使用 @Component 注解,并结合放在类文件本身的 @Configuration 注解,来告诉 Spring 管理这些外部提供的资源。
用 @Component 代替 @Bean?
@Component 注解并不仅限于与外部 API 一起使用。开发者完全允许使用 @Component 注解代替 @Bean 注解来暴露他们自己编写的 JavaBeans。
如果我们从上方的 Score 类中移除 @Bean 注解,我们可以像下面代码中看到的那样,通过使用 @Component 注解来通过 IoC 容器暴露 Score

@Configuration
public class MyRoshamboConfig {
    @Component
    public Score getTheScore() {
        return new Score();
    }
}

何时使用 @Component vs @Bean?
在具有一定规模的 Spring Boot 项目中,我实际上更倾向于使用 @Component 注解而不是 @Bean 注解。这样,配置被限制在单个文件中,而您编写的 JavaBeans 不会被那些将您的源代码紧密绑定到 Spring 框架的注解所充斥。
在较小的项目和原型中?我完全支持使用 @Bean 注解。它更容易使用,并且如果您的项目不需要大量配置,它可以帮助您更快地启动和运行您的微服务。


【注】本文译自:Component vs. Bean annotations in Spring

Java中的多态与继承

Java中的多态与继承

开始学习Java中的多态及如何在多态方法调用中进行方法调用

多态——即对象根据其类型执行特定操作的能力——是Java代码灵活性的核心。四人组(Gang Of Four)创建的许多设计模式都依赖于某种形式的多态,包括命令模式。本文将介绍Java多态的基础知识及如何在程序中使用它。

关于Java多态需要了解的内容

  • 多态与Java继承
  • 为何多态重要
  • 方法重写中的多态
  • 核心Java类中的多态
  • 多态方法调用与类型转换
  • 保留关键字与多态
  • 多态的常见错误
  • 关于多态需要记住的要点

多态与Java继承

我们将重点探讨多态与Java继承的关系。需记住的核心点是:多态需要继承或接口实现。以下示例通过Duke和Juggy展示这一点:

public abstract class JavaMascot {
    public abstract void executeAction();
}

public class Duke extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Punch!");
    }
}

public class Juggy extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Fly!");
    }
}

public class JavaMascotTest {
    public static void main(String... args) {
        JavaMascot dukeMascot = new Duke();
        JavaMascot juggyMascot = new Juggy();
        dukeMascot.executeAction();
        juggyMascot.executeAction();
    }
}

代码输出为:

Punch!
Fly!

由于各自的具体实现,Duke和Juggy的动作均被执行。

为何多态重要

使用多态的目的是将客户端类与实现代码解耦。客户端类通过接收具体实现来执行所需操作,而非硬编码。这种方式下,客户端类仅需了解执行操作的必要信息,这是松耦合的典范。

为了更好地理解多态的优势,请观察以下SweetCreator

public abstract class SweetProducer {
    public abstract void produceSweet();
}

public class CakeProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cake produced");
    }
}

public class ChocolateProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Chocolate produced");
    }
}

public class CookieProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cookie produced");
    }
}

public class SweetCreator {
    private List<SweetProducer> sweetProducer;

    public SweetCreator(List<SweetProducer> sweetProducer) {
        this.sweetProducer = sweetProducer;
    }

    public void createSweets() {
        sweetProducer.forEach(sweet -> sweet.produceSweet());
    }
}

public class SweetCreatorTest {
    public static void main(String... args) {
        SweetCreator sweetCreator = new SweetCreator(
            Arrays.asList(
                new CakeProducer(),
                new ChocolateProducer(),
                new CookieProducer()
            )
        );
        sweetCreator.createSweets();
    }
}

此例中,SweetCreator类仅知晓SweetProducer类,而不了解每个甜点的具体实现。这种分离使类能灵活更新和重用,并大幅提升代码可维护性。设计代码时,应始终寻求使其尽可能灵活和可维护。多态是编写可重用Java代码的强力技术。

提示@Override注解强制程序员使用必须被重写的相同方法签名。若方法未被重写,将产生编译错误。

方法重载是多态吗?

许多程序员对多态与方法重写、重载的关系感到困惑。但只有方法重写是真正的多态。重载共享相同方法名但参数不同。多态是广义术语,因此相关讨论将持续存在。

方法重写中的多态

若返回类型是协变类型,则允许修改重写方法的返回类型。协变类型本质上是返回类型的子类。示例如下:

public abstract class JavaMascot {
    abstract JavaMascot getMascot();
}

public class Duke extends JavaMascot {
    @Override
    Duke getMascot() {
        return new Duke();
    }
}

由于DukeJavaMascot的子类,我们可在重写时修改返回类型。

核心Java类中的多态

我们在核心Java类中频繁使用多态。一个简单示例是实例化ArrayList类时声明List接口为类型:

List<String> list = new ArrayList<>();

进一步观察以下未使用多态的Java集合API代码:

public class ListActionWithoutPolymorphism {
    // 无多态的示例
    void executeVectorActions(Vector<Object> vector) {/* 此处代码重复 */}
    void executeArrayListActions(ArrayList<Object> arrayList) {/* 此处代码重复 */}
    void executeLinkedListActions(LinkedList<Object> linkedList) {/* 此处代码重复 */}
    void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList)
    { /* 此处代码重复 */}
}

public class ListActionInvokerWithoutPolymorphism {
    listAction.executeVectorActions(new Vector<>());
    listAction.executeArrayListActions(new ArrayList<>());
    listAction.executeLinkedListActions(new LinkedList<>());
    listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

这段代码很糟糕,不是吗?想象维护它的难度!现在观察使用多态的相同示例:

public static void main(String … polymorphism) {
    ListAction listAction = new ListAction();    
    listAction.executeListActions();
}
public class ListAction {
    void executeListActions(List<Object> list) {
        // 对不同列表执行操作
    }
}
public class ListActionInvoker {
    public static void main(String... masterPolymorphism) {
        ListAction listAction = new ListAction();
        listAction.executeListActions(new Vector<>());
        listAction.executeListActions(new ArrayList<>());
        listAction.executeListActions(new LinkedList<>());
        listAction.executeListActions(new CopyOnWriteArrayList<>());
    }
}

多态的优势在于灵活性和扩展性。我们无需创建多个不同方法,只需声明一个接收通用List类型的方法。

多态方法调用与类型转换

可以在多态调用中调用特定方法,但会牺牲灵活性。示例如下:

public abstract class MetalGearCharacter {
    abstract void useWeapon(String weapon);
}
public class BigBoss extends MetalGearCharacter {
    @Override
    void useWeapon(String weapon) {
        System.out.println("Big Boss is using a " + weapon);
    }
    void giveOrderToTheArmy(String orderMessage) {
        System.out.println(orderMessage);
    }
}
public class SolidSnake extends MetalGearCharacter {
    void useWeapon(String weapon) {
        System.out.println("Solid Snake is using a " + weapon);
    }
}
public class UseSpecificMethod {
    public static void executeActionWith(MetalGearCharacter metalGearCharacter) {
        metalGearCharacter.useWeapon("SOCOM");
        // 以下行无法工作
        // metalGearCharacter.giveOrderToTheArmy("Attack!");
        if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
    public static void main(String... specificPolymorphismInvocation) {
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

此处使用的技术是类型转换(casting),即在运行时显式改变对象类型。

注意:只有将通用类型强制转换为具体类型后,才能调用特定方法。这相当于明确告诉编译器:“我知道自己在做什么,因此要将对象转换为具体类型并使用特定方法。”

在上述示例中,编译器拒绝接受特定方法调用的原因很重要:传入的类可能是SolidSnake。在此情况下,编译器无法确保每个MetalGearCharacter的子类都声明了giveOrderToTheArmy方法。

保留关键字

注意保留字instanceof。在调用特定方法前,我们需检查MetalGearCharacter是否为BigBoss的实例。若BigBoss实例,将收到以下异常信息:

Exception in thread "main" java.lang.ClassCastException: com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss

若需引用Java超类的属性或方法,可使用保留字super。例如:

public class JavaMascot {
    void executeAction() {
        System.out.println("The Java Mascot is about to execute an action!");
    }
}
public class Duke extends JavaMascot {
    @Override
    void executeAction() {
        super.executeAction();
        System.out.println("Duke is going to punch!");
    }
    public static void main(String... superReservedWord) {
        new Duke().executeAction();
    }
}

在Duke的executeAction方法中使用super可调用超类方法,再执行Duke的特定动作。因此输出如下:

The Java Mascot is about to execute an action!
Duke is going to punch!

多态的常见错误

  • 常见错误是认为无需类型转换即可调用特定方法。
  • 另一个错误是在多态实例化类时不确认将调用哪个方法。需记住:被调用的方法是所创建实例的方法。
  • 还需注意方法重写不同于方法重载
  • 若参数不同,则无法重写方法。若返回类型是超类方法的子类,则可以修改重写方法的返回类型。

关于多态需要记住的要点

  • 所创建的实例将决定使用多态时调用哪个方法。
  • @Override注解强制程序员使用重写方法;否则将产生编译错误。
  • 多态可用于普通类、抽象类和接口。
  • 大多数设计模式依赖某种形式的多态。
  • 调用多态子类中特定方法的唯一方式是使用类型转换。
  • 可通过多态设计强大的代码结构。

接受Java多态挑战!

让我们测试你对多态和继承的理解。在此挑战中,你需要根据Matt Groening的辛普森一家代码推断每个类的输出。首先仔细分析以下代码:

public class PolymorphismChallenge {
    static abstract class Simpson {
        void talk() {
            System.out.println("Simpson!");
        }
        protected void prank(String prank) {
            System.out.println(prank);
        }
    }
    static class Bart extends Simpson {
        String prank;
        Bart(String prank) { this.prank = prank; }
        protected void talk() {
            System.out.println("Eat my shorts!");
        }
        protected void prank() {
            super.prank(prank);
            System.out.println("Knock Homer down");
        }
    }
    static class Lisa extends Simpson {
        void talk(String toMe) {
            System.out.println("I love Sax!");
        }
    }
    public static void main(String... doYourBest) {
        new Lisa().talk("Sax :)");
        Simpson simpson = new Bart("D'oh");
        simpson.talk();
        Lisa lisa = new Lisa();
        lisa.talk();
        ((Bart) simpson).prank();
    }
}

你认为最终输出是什么?不要使用IDE!重点是提升代码分析能力,请自行推断结果。

选项:
A)

I love Sax!  
 D'oh  
 Simpson!  
 D'oh  

B)

Sax :)  
 Eat my shorts!  
 I love Sax!  
 D'oh  
 Knock Homer down  

C)

Sax :)  
 D'oh  
 Simpson!  
 Knock Homer down  

D)

I love Sax!  
 Eat my shorts!  
 Simpson!  
 D'oh  
 Knock Homer down

解答挑战
对于以下方法调用:

new Lisa().talk("Sax :)");

输出为“I love Sax!”,因为我们向方法传递了字符串且Lisa类有此方法。

下一调用:

Simpson simpson = new Bart("D'oh");
simpson.talk();

输出为“Eat my shorts!”,因为我们用Bart实例化了Simpson类型。

以下调用较为复杂:

Lisa lisa = new Lisa();
lisa.talk();

此处通过继承使用了方法重载。由于未向talk方法传递参数,因此调用Simpsontalk方法,输出为:

"Simpson!"

最后一个调用:

((Bart) simpson).prank();

此例中,prank字符串在实例化Bart时通过new Bart("D'oh")传入。此时首先调用super.prank方法,再执行Bart的特定prank方法。输出为:

"D'oh"
"Knock Homer down"

因此正确答案是D。输出为:

I love Sax!
Eat my shorts! 
Simpson!
D'oh
Knock Homer down

【注】本文译自:Polymorphism and inheritance in Java | InfoWorld