Java:如何结合使用instanceof和强制类型转换?

12

(请不要建议我更抽象X并添加另一种方法。)

在C++中,如果我有一个类型为X*的变量x,并且我希望在它也是Y*类型(其中YX的子类)时执行特定操作,我会写下这个:

if(Y* y = dynamic_cast<Y*>(x)) {
    // now do sth with y
}

在Java中似乎不可能做到这一点(或者可以吗?)。

我阅读了这段Java代码:

if(x instanceof Y) {
    Y y = (Y) x;
    // ...
}

有时,当你没有一个变量 x ,而是一个更复杂的表达式时,在Java中由于这个问题,你需要一个虚拟变量:
X x = something();
if(x instanceof Y) {
    Y y = (Y) x;
    // ...
}
// x not needed here anymore

通常情况下,something()iterator.next()。你也可以看到,你真的不能仅仅调用它两次。你确实需要一个虚拟变量。

在这里,你实际上根本不需要 x - 你只是有它,因为你无法一次性进行强制转换的 instanceof 检查。再次将其与相当普遍的 C++ 代码进行比较:

if(Y* y = dynamic_cast<Y*>( something() )) {
    // ...
}

因此,我引入了一个castOrNull函数,可以避免使用虚拟变量x。现在我可以这样写:
Y y = castOrNull( something(), Y.class );
if(y != null) {
    // ...
}

castOrNull的实现:

public static <T> T castOrNull(Object obj, Class<T> clazz) {
    try {
        return clazz.cast(obj);
    } catch (ClassCastException exc) {
        return null;
    }
}

现在,有人告诉我以那种方式使用castOrNull函数是一件邪恶的事情。为什么?(或者更普遍地提问:你是否同意并认为这是邪恶的?如果是,为什么?或者你认为这是一个有效的(也许是罕见的)用例?)

如上所述,我不想讨论使用这种向下转型是否是一个好主意。但让我简要澄清一下为什么有时候我会使用它:

  1. 有时候我会遇到这样的情况:我必须在添加一个非常特定的新方法(仅适用于一个单独的子类和一个特定的案例)之间做出选择,或者使用这样的 instanceof 检查。基本上,我可以选择添加一个函数 doSomethingVeryVerySpecificIfIAmY() 或者进行 instanceof 检查。在这种情况下,我觉得后者更加清晰。

  2. 有时候我有一组某个接口/基类的对象,对于所有类型为 Y 的条目,我想做一些操作,然后将它们从集合中移除。(例如,我曾经遇到过这样的情况:我有一个树形结构,我想删除所有的空叶子节点。)


1
我会使用_instanceOf_而不是_class.cast_来避免_castOrNull_中不必要的异常,但除此之外,你似乎有一个完全正常的帮助程序。让Tom澄清他的意思。 - Nikita Rybak
如果Y是X的超类,你根本不需要进行强制类型转换 - 你是不是指的子类? - Péter Török
虽然这不是Java中的一行代码,但instanceof和casting的性能相当不错。我在Java7中发布了一些关于解决问题不同方法的时间记录:https://dev59.com/KWQo5IYBdhLWcg3wGsPN#28858680 - Wheezil
你的两个例子似乎都违反了子类的“是一个”测试。例如,为什么你的节点没有一个“deleteEmptyChildren”方法呢?我猜是因为你将它与另一个类组合在一起,实际上应该“有一个”指向另一个类的指针。总的来说,我认为你可能过度使用了继承。记住,始终优先选择封装而不是继承。 - Bill K
6个回答

17

从Java 14开始,您应该能够同时执行instanceof和转换。请参见https://openjdk.java.net/jeps/305

代码示例:

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

在上面的例子中,如果instanceof的结果为true,则变量s被定义。该变量的作用域取决于上下文。更多示例请参见上面的链接。

太好了。我知道在过去几年中有些东西被添加到这个里面,但我记不起语法了。 - ArtOfWarfare
这太棒了,我真希望早点知道它的存在。 - Brian_Entei
这太棒了,我希望我一开始就知道这个东西。 - undefined

8
现在,我被告知以这种方式使用castOrNull函数是一件邪恶的事情。为什么?
我可以想到几个原因:
1. 这是一个复杂而棘手的方法,用于完成非常简单的操作。复杂和棘手的代码难以阅读、难以维护,可能会产生错误(当有人不理解它时),因此是邪恶的。
2. castOrNull方法的复杂和棘手的方式很可能无法通过JIT编译器进行优化。你最终将得到至少3个额外的方法调用,加上大量额外的代码来进行类型检查和反射转换。不必要地使用反射是邪恶的。
与此相比,简单的方式(使用instanceof后跟类转换)使用特定的字节码进行instanceof和类转换。字节码序列几乎肯定会被优化,以便在本机代码中只进行一次空检查和一次对象类型测试。这是一个常见的模式,应该很容易被JIT编译器检测和优化。
当然,“邪恶”只是另一种说法,即您真的不应该这样做。
您添加的两个示例中,都不需要或不可取使用castOrNull方法。在我看来,“简单的方式”从可读性和性能的角度来看都更好。

啊,谢谢你提供的信息!那么你也认为我不应该使用castOrNull吗?或者你如何评价那些反对我的观点呢?(毕竟,避免使用虚拟变量也可以使代码更易读和更短。) - Albert
1
我能看出的唯一优点是在某些情况下避免使用临时变量。对我来说,这只是一个纯粹的外观问题。在我看来,使用临时变量并不会使代码更难读,而且更短的代码也不一定是可读性的优势。(顺便说一句,它并不是一个“虚拟”变量,因为它确实被使用了。) - Stephen C

5
在大多数编写/设计良好的Java代码中,使用instanceof和强制转换是不必要的。随着泛型的引入,许多强制转换(因此包括instanceof)的情况都不再需要。尽管偶尔还是会发生。
castOrNull方法是邪恶的,因为它使Java代码看起来“不自然”。从一种语言转到另一种语言时最大的问题是采用新语言的惯例。在Java中,临时变量完全没问题。事实上,你的方法只是隐藏了临时变量。
如果你发现自己写了很多强制转换,应该检查一下代码,看看为什么需要这样做,并寻找消除它们的方法。例如,在你提到的情况下,添加一个“getNumberOfChildren”方法将允许你检查节点是否为空,从而能够在不进行强制转换的情况下剪枝(这只是一个猜测,在这种情况下可能不适用于你)。
总的来说,在Java中强制转换是“邪恶”的,因为通常是不必要的。你的方法更加“邪恶”,因为它没有按照大多数人期望的Java代码书写方式来编写。
话虽如此,如果你想这么做,就去做吧。它实际上并不是“邪恶”的,只是在Java中不是“正确”的做法。

4

在我看来,你的castOrNull并不是邪恶的,只是没有意义。你似乎着迷于摆脱一个临时变量和一行代码,而对我来说更重要的问题是:为什么你的代码中需要这么多向下转型?在面向对象编程中,这几乎总是优化设计的症状。我更喜欢解决根本原因,而不是治疗症状。


1
在Java中有很多我们不得不使用的库,而不是自己设计。(迭代器就是其中一个例子) - Nikita Rybak
@Nikita,说得对。如果您使用设计不良但同时又是必不可少的Java库,则几乎没有选择余地。我个人猜测这种库应该非常罕见。如果一个库使用起来如此笨拙,那么要么没有人使用它,要么迟早会有人开始开发更好的替代品。 - Péter Török
我扩展了我的问题以展示两种情况,我有时会使用它。我不是真的喜欢绝对禁止某些东西的硬规则,我认为我提供的两种情况是有效的情况,在这种情况下你可以像这样做。你可能比我更极端,但我想你也不会极端到永远不使用这样的代码。然后,如果你碰巧有这样的情况,我总是喜欢有最干净的解决方案,这就是我在这里提问的原因。 :) - Albert
@Albert,我也不喜欢在SO上说“永远”这个词;-) 我的职业生涯大部分时间都在处理遗留代码,所以我必须学会实用主义,接受次优解决方案。使用instanceof是一种标准惯用语,易于任何人理解,并且 - 正如Stephen C指出的那样 - 易于优化。而且我个人对临时变量没有任何问题。 - Péter Török

2

我不确定那个人为什么认为它是邪恶的。然而,可能是因为你在转换之前没有检查类型,而是在之后捕获了异常。以下是一种解决方法。

public static <T> T castOrNull(Object obj, Class<T> clazz) {
    if ( clazz.isAssignableFrom(obj.getClass()) ) {
        return clazz.cast(obj);
    } else {
        return null;
    }
}

我怀疑他的意思不是那样,因为我要求更好的代码,而我在这里发布的代码是他自己的建议。 - Albert
在链接的答案评论中讨论了一个类似的替代方案,Tom并不比上面的变体更喜欢它。 - Péter Török
@Albert:没注意到这个……既然是这种情况,我会说很可能就像Péter的回答所说,这是一个次优设计的问题。但是,为了确切地弄清楚他为什么认为这样做是错误的,你必须等待他的回复(如果他回复你的话)。 - Thomas

0

Java异常很慢。如果您试图通过避免双重转换来优化性能,而使用异常代替逻辑,那么您就是在自己的脚上开枪。永远不要依赖于捕获异常来处理您可以合理检查和纠正的问题(这正是您正在做的事情)。

Java异常有多慢?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接