接口和类型
interface
关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口:
public interface A {
void f();
}
然后实现这个接口,你可以看到其代码是怎么从实际类型开始顺藤摸瓜的:
class B implements A {
@Override
public void f() {
}
public void g() {
}
}
public class InterfaceViolation {
public static void main(String[] args) {
A a = new B();
a.f();
// a.g(); // Compile error
System.out.println(a.getClass().getName());
if (a instanceof B) {
B b = (B) a;
b.g();
}
}
}
输出结果:
com.example.test.B
通过使用 RTTI,我们发现 a
是用 B
实现的。通过将其转型为 B
,我们可以调用不在 A
中的方法。
这样的操作完全是合情合理的,但是你也许并不想让客户端开发者这么做,因为这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 interface
关键字正在保护你,但其实并没有。另外,在本例中使用 B
来实现 A
这种情况是有公开案例可查的。
一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。这在很多情况下都是可行的,但“可能”还不够,你或许希望能有一些更严格的控制方式。
最简单的方式是让实现类只具有包访问权限,这样在包外部的客户端就看不到它了:
class C implements A {
@Override
public void f() {
System.out.println("public C.f()");
}
public void g() {
System.out.println("public C.g()");
}
void u() {
System.out.println("package C.u()");
}
protected void v() {
System.out.println("protected C.v()");
}
private void w() {
System.out.println("private C.w()");
}
}
public class HiddenC {
public static A makeA() {
return new C();
}
}
在这个包中唯一 public
的部分就是 HiddenC
,在被调用时将产生 A
接口类型的对象。这里有趣之处在于:即使你从 makeA()
返回的是 C
类型,你在包的外部仍旧不能使用 A
之外的任何方法,因为你不能在包的外部命名 C
。
现在如果你试着将其向下转型为 C
,则将被禁止,因为在包的外部没有任何 C
类型可用:
import java.lang.reflect.*;
public class HiddenImplementation {
public static void main(String[] args) throws Exception {
A a = HiddenC.makeA();
a.f();
System.out.println(a.getClass().getName());
// Compile error: cannot find symbol 'C':
/* if(a instanceof C) {
C c = (C)a;
c.g();
} */
// Oops! Reflection still allows us to call g():
callHiddenMethod(a, "g");
// And even less accessible methods!
callHiddenMethod(a, "u");
callHiddenMethod(a, "v");
callHiddenMethod(a, "w");
}
static void callHiddenMethod(Object a, String methodName) throws Exception {
Method g = a.getClass().getDeclaredMethod(methodName);
g.setAccessible(true);
g.invoke(a);
}
}
输出结果:
正如你所看到的,通过使用反射,仍然可以调用所有方法,甚至是 private
方法!如果知道方法名,你就可以在其 Method
对象上调用 setAccessible(true)
,就像在 callHiddenMethod()
中看到的那样。
本章小结
RTTI 允许通过匿名类的引用来获取类型信息。初学者极易误用它,因为在学会使用多态调用方法之前,这么做也很有效。有过程化编程背景的人很容易把程序组织成一系列 switch
语句,你可以用 RTTI 和 switch
实现功能,但这样就损失了多态机制在代码开发和维护过程中的重要价值。面向对象编程语言是想让我们尽可能地使用多态机制,只在非用不可的时候才使用 RTTI。
然而使用多态机制的方法调用,要求我们拥有基类定义的控制权。因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来自别人的库,这时 RTTI 便是一种解决之道:可继承一个新类,然后添加你需要的方法。在代码的其它地方,可以检查你自己特定的类型,并调用你自己的方法。这样做不会破坏多态性以及程序的扩展能力,因为这样添加一个新的类并不需要修改程序中的 switch
语句。但如果想在程序中增加具有新特性的代码,你就必须使用 RTTI 来检查这个特定的类型。
如果只是为了方便某个特定的类,就将某个特性放进基类里边,这将使得从那个基类派生出的所有其它子类都带有这些可能毫无意义的东西。这会导致接口更加不清晰,因为我们必须覆盖从基类继承而来的所有抽象方法,事情就变得很麻烦。举个例子,现在有一个表示乐器 Instrument
的类层次结构。
假设我们想清理管弦乐队中某些乐器残留的口水,一种办法是在基类 Instrument
中放入 clearSpitValve()
方法。但这样做会导致类结构混乱,因为这意味着打击乐器 Percussion
、弦乐器 Stringed
和电子乐器 Electronic
也需要清理口水。在这个例子中,RTTI 可以提供一种更合理的解决方案。可以将 clearSpitValve()
放在某个合适的类中,在这个例子中是管乐器 Wind
。不过,在这里你可能会发现还有更好的解决方法,就是将 prepareInstrument()
放在基类中,但是初次面对这个问题的读者可能想不到还有这样的解决方案,而误认为必须使用 RTTI。
最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题(比如使用 Profiler)。
我们已经看到,反射,因其更加动态的编程风格,为我们开创了编程的新世界。但对有些人来说,反射的动态特性却是一种困扰。对那些已经习惯于静态类型检查的安全性的人来说,Java 中允许这种动态类型检查(只在运行时才能检查到,并以异常的形式上报检查结果)的操作似乎是一种错误的方向。
有些人想得更远,他们认为引入运行时异常本身就是一种指示,指示我们应该避免这种代码。我发现这种意义的安全是一种错觉,因为总是有些事情是在运行时才发生并抛出异常的,即使是在那些不包含任何 try
语句块或异常声明的程序中也是如此。因此,我认为一致性错误报告模型的存在使我们能够通过使用反射编写动态代码。当然,尽力编写能够进行静态检查的代码是有价值的,只要你有这样的能力。但是我相信动态代码是将 Java 与其它诸如 C++ 这样的语言区分开的重要工具之一。