赛迪网 > IT技术 Java > (最新更新)Java基础
  IT资讯搜索
 
IT产品搜索
[程序开发][网管世界][网络安全][数据库技术]
[操作系统][嘉宾聊天·在线访谈][活动集锦]
[精彩专题][Symantec专区][订阅IT技术周刊]
[开发论坛][网管论坛][安全论坛][数据库论坛]
[操作系统论坛][Sybase专区][IBM dW技术专区]
[病毒求助][病毒与漏洞播报][文档·源码下载]

JAVA中用动态代理类实现记忆功能(二)

发布时间:2006.03.15 18:25     来源:Matrix    作者:chris

通过动态代理类来创建一个通用的缓存包装器
   上面第二种方法仅有的缺点就是缓存包装器不能重用,每次我们希望添加一个缓存给某个类,我们就要写一个特殊的缓存包装器给目标接口.这是一个很慢,容易出错的过程.
     Jdk1.3开始支持动态代理类: 特别的类能够在运行期决定实现哪个接口-通常的模式都是,在运行期即决定实现哪个接口.通过这个,我们有可能实现一个通用的缓存包装器,我们称它为Memoizer,在运行期决定实现哪个接口.这样, CachingBinaryDigitsCalculator就是不再需要的.它是这样被调用的:

BinaryDigitsCalculator calculator =
  new CachingBinaryDigitsCalculator(
    new PiBinaryDigitsCalculator()
  );

可以通过Memoizer来重写如下:
BinaryDigitsCalculator calculator =
  (BinaryDigitsCalculator) Memoizer.memoize(
    new PiBinaryDigitsCalculator()
  );

Memoizer的代码如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Memoizer implements InvocationHandler {
  public static Object memoize(Object object) {
      return Proxy.newProxyInstance(
        object.getClass().getClassLoader(),
        object.getClass().getInterfaces(),
        new Memoizer(object)
      );
  }

  private Object object;
  private Map caches = new HashMap();

  private Memoizer(Object object) {
      this.object = object;
  }

  public Object invoke(Object proxy, Method method,
  Object[] args) throws Throwable {

      if (method.getReturnType().equals(Void.TYPE)) {
          // Don't cache void methods
          return invoke(method, args);
      } else {
          Map cache    = getCache(method);
          List key     = Arrays.asList(args);
          Object value = cache.get(key);

          if (value == null && !cache.containsKey(key)) {
              value = invoke(method, args);
              cache.put(key, value);
          }
          return value;
      }
  }

  private Object invoke(Method method, Object[] args)
  throws Throwable {
      try {
          return method.invoke(object, args);
      } catch (InvocationTargetException e) {
          throw e.getTargetException();
      }
  }

  private synchronized Map getCache(Method m) {
      Map cache = (Map) caches.get(m);
      if (cache == null) {
          cache = Collections.synchronizedMap(
            new HashMap()
          );
          caches.put(m, cache);
      }
      return cache;
  }
}

    当调用静态方法memoize的时候,将会创建一个新的代理实例-也就是一个java.lang.reflect.proxy的实例.实现了一个接口集.这个接口集由object.getClass().getInterfaces()来决定.每个代理实例包含一个java.lang.reflect.InvocationHandler实例来处理这个代理实例调用的相关方法.在我们的例子里,Memoizer就是一个InvocationHandler实例.
    当一个方法在代理实例里被调用,比如, calculateBinaryDigit,那么, Memoizer实例里的invoke方法就会被调用,相关信息会传给invoke方法,以决定proxy实例调用了哪个方法,包含参数信息.在我们的例子里,传入Memoizer的java.lang.Method参数是calculateBinaryDigit,而参数信息则是pi需要精确的位数-整数n.在这个基础上,Memoizer能够进一步进行缓存操作的.
    在例子里(caches是一个Hashmap,cache是一个map)里用到的Key,主要是传入的方法信息:Method对象和参数对象. 为了实现的简单与通用性,Memoizer有一个关于cache的HashMap caches,每个method是一个key,对应的value为一个cache.然后把参数信息转化成一个List对象,作为cache的Key.使用List是很方便的,同时也可以保证equals()方法,所以能够保证当且仅当参数信息完全相同的时候这个List才相等.
    一旦一个cache的Key被创建,那么,计算之前都会先查找这个cache,如果找到,则返回cache里的值.否则,如果带有这些参数的这个方法没有被调用过,那么,则会通过invoke来调用这个method.在我们的例子里, 实例PiBinaryDigitsCalculator 里的calculateBinaryDigit方法将会通过invoke被调用.而且计算结果将会被存在cache里.

何时使用Memoizer
    作为一条通用的规则,Memoizer能够在任何需要传统的cache的时候使用-比如上面提到的例子. 特别地,接口里每个需要使用记忆功能的method需要满足下面几条条件:
1.        这个method的返回值最好不要每次调用都会改变
2.        这个method不要有副效应
3.        这个method的参数是确定的,非mutable的.

     显然,如果每次调用这个method返回值都不同,那么cache就毫无用处了.同样也是很重要的一点是,因为有副效应的method不会被重复,所以这个method不能有副效应(method自动更新某些状态).当然,void方法除外.

     同样,memorize一个带有未定(mutable)参数的method是很危险的,因为,要把这些参数储存到hashmap里会是很危险的一件事.根据Map的定义,当这个Map里的key发生改变,Map是不知道的.所以,当你执行了一次这个method之后,相关信息添加进了Map,然后参数发生变异(mutate),第二次调用的时候,就会得到错误的结果.

性能
    使用cache的主要目的就是为了提升你的程序的速度.然而,reflection确是众所周知的低效(在jdk1.4里有所改进,通过reflection调用方法是普通调用速度的1/2,这个比jdk1.3要快40倍).Memoizer主要依靠reflection来调用方法,所以,它看上去并不是一个好的途径.但是,如果使用cache能给程序速度带来的提升远高于reflection对速度的影响,那么,使用Memoizer是值得考虑的.
    在我们对PiBinaryDigitsCalculator的测试中,测试环境为jdk1.4,当n小于10的时候,使不使用cache速度是相当的.但是,当n增大的时候,使用cache的优势就开始显示出来.所以,经常使用PiBinaryDigitsCalculator的用户,可以考虑使用cache.
    不幸的是,唯一测试你的程序是否需要cache的途径是比较你的程序在两种情况下的运行效率.尽管如此,因为为一个程序构造一个cache包装器是很容易的一件事,移除它也是很容易的,下面的建议可以作为一个参考的步骤:
1.        选择需要记忆操作的类
2.        运行它
3.        如果效率是满意的,go to 6
4.        添加memoizer,使用cache
5.        如果效率没有显著提升,移初memoizer
6.        如果需要,重试.

    理论上,你需要分析为一个类添加记忆功能对整个系统的影响.只有你自己清楚是否值得添加.有些方法,即使是计算量很大的,但是在这个系统里很少被调用,所以,没必要为它添加记忆功能.为了保证这个,我开发了一个更有特点的Memoizer,实现了一个叫做CacheStatistics的接口,你能从它得到cache的数量以及无效的cache.你可以使用它作为判断的一个尺度.

扩展Memoizer

    修改Memoizer类来支持不同的cache策略是很简单的.一个比较普通的类型就是Least-Recently-Used(LRU)cahce,拥有固定数量的入口.这个cache确保入口不大于它的最大数目,如果超过,就摒弃最旧的缓存数据.也就是,能够从cache里得到的是新的数据.一个类可以使用LRU cache来防止一个程序长期保持一个状态.你可以仅仅传递一个参数给CacheFactory里的memoize方法来选择你需要的cache类型.下面的例子,LRU cache最多有1000个入口:
BinaryDigitsCalculator calculator =
  (BinaryDigitsCalculator) Memoizer.memoize(
      new PiBinaryDigitsCalculator(),
      new LruCacheFactory(1000)
  );
  
即使是这么简单,Memoizer也应该是java程序员一个有用的工具.

参考资源:
•        Joshua Bloch, Effective Java Programming Language Guide. Addison Wesley Professional (2001). Contains useful advice about optimization.
•        Patrick Chan, The Java Developers Almanac 1.4. Addison Wesley Professional (2002). Full of Java recipes, including how to write a LRU cache.
•        Paul Graham, On Lisp. Prentice Hall (1993). How to write a memoizing function in Common Lisp.
•        Alex Martelli and David Ascher (Eds.), Python Cookbook. O'Reilly (July 2002). Recipe 17.7, Memoizing (Caching) the Return Values of Functions shows you how to memoize in Python.
•        Documentation for LinkedHashMap in Java SDK 1.4
•        Documentation for Proxy in Java SDK 1.4
•        The Memoizer Framework

关于翻译作者
chris,热衷于java游戏引擎技术,jvm技术,活跃于jxta社区.可以点击:
http://www.matrix.org.cn/user_view.asp?username=chris查看他的个人信息


[ 发表评论 ] 字体[  ] [ 打印 ] [ 进入博客 ] [ 进入论坛 ]  [ 推荐给朋友 ]
  相关文章
· 深入理解abstract class和interface (02-02) · CVS常用命令速查手册 (10-23)
· Java 理论与实践:哈希 (10-23) · Ant实践 (05-27)
· jvm学习笔记之一 (05-19) · Java FTP客户端类库比较(一) (05-21)
· 当 Hibernate 遇上 Spring (03-15) · 具有Applet & Application 双重身份的类 (09-08)
· 用Java多媒体框架实现自播放 (06-21) · 非阻塞套接字(Nonblocking Sockets) 概述 (03-12)
  客户需求反馈表
* 姓  名:
更多资料  了解方案  认识厂商
* 单位名称:
* 联系电话:
* 电子邮件:
  赛迪推荐  
  手机·资费 ·新品·导购·评测·手机资费·宽带
手机搜索  诺基亚 N73 MOTO Z6
  IT产品 ·笔记本·台式机·服务器·打印·投影
IT产品搜索 
  IT技术 ·开发·网管·安全·数据库·操作系统
  信息化 ·热点·专题·访谈·周刊·方案案例
· 电子商务盈利新招:垂直化渠道管理新趋势
· 互联网加速蜕变 运营商进军搅动市场格局
· 08 IT技术风起云涌 专访 ITIL落地难在哪
· 视频加密保护解决方案 文档安全解决方案
  IT博客 ·曾剑秋·项立刚·Java学习·网管
  IT技术论坛 ·开发·网管·安全·数据库·系统