文章目录
- 第十九章 类型信息
- 7. 动态代理
- 8. Optional类
- 9. 接口和类型
- 10. 本章小结
第十九章 类型信息
7. 动态代理
代理是基本的设计模式之一。一个对象封装真实对象,代替其提供其他或不同的操作—这些操作通常涉及到与“真实”对象的通信,因此代理通常充当中间对象。这是一个简单的示例,显示代理的结构:
interface Interface {
void doSomething();
void somethingElse(String arg);
}
class RealObject implements Interface {
@Override
public void doSomething() {
System.out.println("doSomething");
}
@Override
public void somethingElse(String arg) {
System.out.println("somethingElse " + arg);
}
}
class SimpleProxy implements Interface {
private Interface proxied;
public SimpleProxy(Interface proxied) {
this.proxied = proxied;
}
@Override
public void doSomething() {
System.out.println("SimpleProxy doSomething");
proxied.doSomething();
}
@Override
public 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;
}
@Override
public 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;@3b9a45b3
bonobo
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 {
@Override
public 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 {
@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 类型可用:
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 {
@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 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?";
@Override
public 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动态代理类和动态代理对象
(图网,侵删)