文章目录
- 第十九章 类型信息
- 7. 动态代理
- 8. Optional类
- 9. 接口和类型
- 10. 本章小结
第十九章 类型信息
7. 动态代理
代理是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作—这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。这是一个简单的示例,显示代理的结构:
interface Interface {void doSomething();void somethingElse(String arg);
}class RealObject implements Interface {@Overridepublic void doSomething() {System.out.println("doSomething");}@Overridepublic void somethingElse(String arg) {System.out.println("somethingElse " + arg);}
}class SimpleProxy implements Interface {private Interface proxied;public SimpleProxy(Interface proxied) {this.proxied = proxied;}@Overridepublic void doSomething() {System.out.println("SimpleProxy doSomething");proxied.doSomething();}@Overridepublic void somethingElse(String arg) {System.out.println("SimpleProxy somethingElse " + arg);proxied.somethingElse(arg);}
}public class SimpleProxyDemo {public static void consumer(Interface iface) {iface.doSomething();iface.somethingElse("bonobo");}public static void main(String[] args) {consumer(new RealObject());System.out.println("------------");consumer(new SimpleProxy(new RealObject()));}
}
输出:
doSomething
somethingElse bonobo
------------
SimpleProxy doSomething
doSomething
SimpleProxy somethingElse bonobo
somethingElse bonobo
Java 的动态代理更进一步,不仅动态创建代理对象而且动态处理对代理方法的调用。在动态代理上进行的所有调用都被重定向到单个调用处理程序,该处理程序负责发现调用的内容并决定如何处理。这是 SimpleProxyDemo.java 使用动态代理重写的例子:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;class DynamicProxyHandler implements InvocationHandler {private Object proxied;public DynamicProxyHandler(Object proxied) {this.proxied = proxied;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("**** proxy: " + proxy.getClass() +", method: " + method + ", args: " + args);if (args != null) {for (Object arg : args) {System.out.println(" " + arg);}}return method.invoke(proxied, args);}
}public class SimpleDynamicProxy {public static void consumer(Interface iface) {iface.doSomething();iface.somethingElse("bonobo");}public static void main(String[] args) {RealObject real = new RealObject();consumer(real);System.out.println("-------------");Interface proxy = (Interface) Proxy.newProxyInstance(Interface.class.getClassLoader(),new Class[]{Interface.class},new DynamicProxyHandler(real));consumer(proxy);}
}
输出:
doSomething
somethingElse bonobo
-------------
**** proxy: class typeinfo.$Proxy0, method: public abstract void typeinfo.Interface.doSomething(), args: null
doSomething
**** proxy: class typeinfo.$Proxy0, method: public abstract void typeinfo.Interface.somethingElse(java.lang.String), args: [Ljava.lang.Object;@3b9a45b3bonobo
somethingElse bonobo
可以通过调用静态方法 Proxy.newProxyInstance() 来创建动态代理,该方法需要一个类加载器(通常可以从已加载的对象中获取),希望代理实现的接口列表(不是类或抽象类),以及接口 InvocationHandler 的一个实现。
8. Optional类
Optional 对象可以防止你的代码直接抛出 NullPointException 。
9. 接口和类型
interface 关键字的一个重要目标就是允许程序员隔离组件,进而降低耦合度。使用接口可以实现这一目标,但是通过类型信息,这种耦合性还是会传播出去——接口并不是对解耦的一种无懈可击的保障。比如我们先写一个接口:
package typeinfo.interfacea;public interface A {void f();
}
然后实现这个接口,你可以看到其代码是怎么从实际类型开始顺藤摸瓜的:
package typeinfo;import typeinfo.interfacea.A;class B implements A {@Overridepublic void f() {}public void g() {}
}public class InterfaceViolation {public static void main(String[] args) {A a = new B();a.f();// a.g(); // 报错System.out.println(a.getClass().getName());if(a instanceof B){B b = (B) a;b.g();}}
}
输出:
typeinfo.B
通过使用 RTTI,我们发现 a 是用 B 实现的。通过将其转型为 B ,我们可以调用不在 A 中的方法。这给了他们一个机会,使得他们的代码与你的代码的耦合度超过了你的预期。也就是说,你可能认为 interface 关键字正在保护你,但其实并没有。
一种解决方案是直接声明,如果开发者决定使用实际的类而不是接口,他们需要自己对自己负责。
最简单的方式是让实现类只具有包访问权限,这样在包外部的客户端就看不到它了:
package typeinfo;class C implements A {@Overridepublic 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 类型可用:
package typeinfo;import typeinfo.interfacea.A;
import typeinfo.packageaccess.HiddenC;import java.lang.reflect.Method;public class HiddenImplementation {public static void main(String[] args) throws Exception {A a = HiddenC.makeA();a.f(); // PS:只能点出 f()System.out.println(a.getClass().getName());
// if(a instanceof C){
// C c = (C) a;
// c.g();
// }callHiddenMethod(a, "g");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);}
}
输出:
public C.f()
typeinfo.packageaccess.C
public C.g()
package C.u()
protected C.v()
private C.w()
通过使用反射,仍然可以调用所有方法,甚至是 private 方法!如果知道方法名,你就可以在其 Method 对象上调用 setAccessible(true) ,就像在 callHiddenMethod() 中看到的那样。
你可能觉得,可以通过只发布编译后的代码来阻止这种情况,但其实这并不能解决问题。因为只需要运行 javap (一个随 JDK 发布的反编译器)即可突破这一限制。
那如果把接口实现为一个私有内部类,又会怎么样呢?下面展示了这种情况:
package typeinfo;import typeinfo.interfacea.A;class InnerA {private static class C implements A {@Overridepublic 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 static A makeA() {return new C();}
}public class InnerImplementation {public static void main(String[] args) throws Exception {A a = InnerA.makeA();a.f();System.out.println(a.getClass().getName());HiddenImplementation.callHiddenMethod(a, "g");HiddenImplementation.callHiddenMethod(a, "u");HiddenImplementation.callHiddenMethod(a, "v");HiddenImplementation.callHiddenMethod(a, "w");}
}
输出:
public C.f()
typeinfo.InnerA$C
public C.g()
package C.u()
protected C.v()
private C.w()
这里对反射仍然没有任何东西可以隐藏。那么如果是匿名类呢?
package typeinfo;import typeinfo.interfacea.A;class AnonymousA {public static A makeA() {return new A() {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 AnonymousImplementation {public static void main(String[] args) throws Exception {A a = AnonymousA.makeA();a.f();System.out.println(a.getClass().getName());HiddenImplementation.callHiddenMethod(a, "g");HiddenImplementation.callHiddenMethod(a, "u");HiddenImplementation.callHiddenMethod(a, "v");HiddenImplementation.callHiddenMethod(a, "w");}
}
看起来任何方式都没法阻止反射调用那些非公共访问权限的方法。对于字段来说也是这样,即便是 private 字段:
package typeinfo;import java.lang.reflect.Field;class WithPrivateFinalField {private int i = 1;private final String s = "I'm totally safe";private String s2 = "Am I safe?";@Overridepublic String toString() {return "i = " + i + ", " + s + ", " + s2;}
}public class ModifyingPrivateFields {public static void main(String[] args) throws Exception {WithPrivateFinalField pf = new WithPrivateFinalField();System.out.println(pf);Field f = pf.getClass().getDeclaredField("i");f.setAccessible(true);System.out.println("f.getInt(pf): " + f.getInt(pf));f.setInt(pf, 47);System.out.println(pf);f = pf.getClass().getDeclaredField("s");f.setAccessible(true);System.out.println("f.get(pf): " + f.get(pf));f.set(pf, "No, you're not!");System.out.println(pf);f = pf.getClass().getDeclaredField("s2");f.setAccessible(true);System.out.println("f.get(pf): " + f.get(pf));f.set(pf, "No, you're not!");System.out.println(pf);}
}
输出:
i = 1, I'm totally safe, Am I safe?
f.getInt(pf): 1
i = 47, I'm totally safe, Am I safe?
f.get(pf): I'm totally safe
i = 47, I'm totally safe, Am I safe?
f.get(pf): Am I safe?
i = 47, I'm totally safe, No, you're not!
但实际上 final 字段在被修改时是安全的。运行时系统会在不抛出异常的情况下接受任何修改的尝试,但是实际上不会发生任何修改。
—PS:final 不可修改,例如上面的变量 s
推荐大佬文章:
Java反射(通俗易懂)
Java动态代理
什么是java rtti_浅析Java RTTI 和 反射的概念
10. 本章小结
RTTI 允许通过匿名类的引用来获取类型信息。
面向对象编程语言是想让我们尽可能地使用多态机制,只在非用不可的时候才使用 RTTI。
然而使用多态机制的方法调用,要求我们拥有基类定义的控制权。因为在你扩展程序的时候,可能会发现基类并未包含我们想要的方法。如果基类来自别人的库,这时 RTTI 便是一种解决之道:可继承一个新类,然后添加你需要的方法。
最后一点,RTTI 有时候也能解决效率问题。假设你的代码运用了多态,但是为了实现多态,导致其中某个对象的效率非常低。这时候,你就可以挑出那个类,使用 RTTI 为它编写一段特别的代码以提高效率。然而必须注意的是,不要太早地关注程序的效率问题,这是个诱人的陷阱。最好先让程序能跑起来,然后再去看看程序能不能跑得更快,下一步才是去解决效率问题(比如使用 Profiler)5。
我们已经看到,反射,因其更加动态的编程风格,为我们开创了编程的新世界。我相信动态代码是将 Java 与其它诸如 C++ 这样的语言区分开的重要工具之一。
自我学习总结:
- RTTI 是在编译期,反射是在运行期
- 通过反射可以获取类的所有方法和字段
- 在java的java.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象
(图网,侵删)