十一月's profileNOVEMBREBlogListsGuestbookMore Tools Help

Blog


    February 22

    NOKIA暗码大全


        暗 码       用 途        备 注
     *#06#      查询IMEI号码     所有手机通用
     *#7370#     格式化手机      Series 60手机专用
     *#7780#     恢复出厂设置     Series 60和Series 40手机通用
     *#0000#     查询当前软件版本号  所有手机通用
     *#7760#     查询生产线号码    Series 40手机专用
     *#2820#     查询蓝牙设备地址   Series 60和Series 40手机通用
     *3370#      激活EFR       部分型号的手机可用
     #3370#      关闭EFR       部分型号的手机可用
     *4720#      激活HFR       部分型号的手机可用
     #4720#      关闭HFR       部分型号的手机可用
    *#92702689#    查询总通话时间    仅限6630
    *#92702689#    进入数据模式     Series 40手机专用
    *#7370925538#   为手机上锁      Series 60手机专用
    一、*#06#:显示IMEI码

      二、*#0000#:显示软件版本
      (部分型号如果不起作用,可按*#型号代码#,如*#6110#)
      第一行--软件版本;
      第二行--软件发布日期;
      第三行--手机型号

      三、*#92702689#:显示序列号和IMEI码,带滚动菜单
      第一屏表示 - 手机出厂时间:1197(月月年年);
      第二屏表示 - 串号和IMEI;
      第三屏表示 - 购买时间:1197(月月年年)可以更改一次
      (但一经输入就不能再作更改,日期会被永久记录)
      第四屏表示 - 最后修理时间:0000(月月年年)维修时设置。
      第五屏表示 - 传输用户数据:退出关闭电话,然后返回。
      如果以上工作不正常,并且电话要求您输入一个合法的代码,可试试:
      1).6232(OK):显示制造的月和年
      2).7832(OK):显示手机购买时间的月和年
      3).7332(OK):显示最近维修时间的月和年:0000(月月年年)维修时设置
      4).9268(OK):显示系列号
      5).37832(OK):设置购买时间的月和年(警告:只允许做一次)
      6).87267(OK):确认传输,与软件更新有关

      四、速率编码
      *3370#或*efr#:开启(EFR)全速率编码
      #3370#或#efr#:关闭全速率编码
      (开启全速增强型编码模式,可改善话音质量但会耗电)
      *4720#或*hra0#:开启(HR)半速率编码
      #4720#或#hra0#:关闭半速率编码
      (话音质量降低,但可延长电池大概30%左右使用时间,需网络支持)
      键入这些代码后,会关机重开,然后才能生效。

      五、SIM卡锁信息
      *#746025625# (= *#sim0clock#):如果SIM卡能被锁,进行检测时键入该指令后,手机显示\"SIM CLOCK STOP ALLOWED\" or \"SIM CLOCK STOP NOT ALLOWED\",这取决于你的SIM卡。
      SIM卡锁信息:包括四种不同的锁
      1).国家锁--锁指定的国家
      2).网络锁--锁指定的网络
      3).供应商锁--锁服务提供商
      4).SIM卡锁--锁指定的SIM卡

      六、查询手机是否锁频
      首先必须找出设定手机时必须使用的几个键。其中,连续按*键二次即出现\"+\";连续按*键三次即出现\"p\";连续按*键四次即出现\"w\"。然后,你就可以依次顺序输入相应组合键。
      #pw+1234567890+1#:查询是否锁国家码
      #pw+1234567890+2#:查询是否锁网络码
      #pw+1234567890+3#:查询是否锁网络提供者锁定的码
      #pw+1234567890+4#:查询是否锁SIM卡

    优秀的J2ME网站集锦

    http://java.sun.com/javame/
    http://www.theserverside.com/
    http://www.onhandset.com/
    http://billday.com/j2me/

    http://java.sun.com/j2me/index.jsp
    SUN公司J2ME平台的官方站点,许多权威资料都可以在此找到。
    http://jcp.org/en/jsr/tech?listBy=1&listByType=platform
    J2ME平台的所有JSR规范,这是jcp的官方站点。
    http://www.microjava.com
    非常全面的j2me开发站点,内容丰富,包括各种示例,还能找到许多设备的资料。
    http://www.onjava.com/topics/java/Wireless_Java
    O'Reilly onjava的j2me版,以文章教程为主。
    http://www.corej2me.com/
    以书籍为主的j2me站点
    http://www.wirelessdevnet.com/
    又一个教程/下载都很丰富的j2me开发站点
    http://www.mobilegd.com/
    主要以J2ME游戏开发为主的站点
    中文:相对于英文站点,国内专注于J2ME的技术站点还特别少,重点推荐以下站点:
    http://www-128.ibm.com/developerworks/cn/java/index.html
    IBM开发者社区也有不少不错的J2ME教程。
    http://j2me1.motorola.com.cn/index_ch.asp
    MOTO的中文开发站点,可以下载到moto的SDK
    各大手机厂商的开发站点:
    Nokia: http://www.forum.nokia.com
    Moto: http://www.motocoder.com
    Sony-Ericcson: http://developer.sonyericsson.com/site/global/home/p_home.jsp
    手机应用下载/手机游戏下载:
    http://www.imobile.com.cn/
    手机之家
    http://www.mbook.com.cn
    掌上书院
    http://java.sun.com/
    Java Software 网站,其中有最新的 Java 技术、产品信息、新闻和功能信息。
    http://java.sun.com/products/jdk/1.2/index.html
    JDK 1.2 产品和下载页
    http://java.sun.com/docs
    Java 平台文档提供对白皮书、Java 教程和其它文档的访问。
    http://developer.java.sun.com/
    Java 开发人员连接网站(需要免费注册)。其它技术信息、新闻和功能;用户论坛;支持信息及其它。
    http://java.sun.com/products/
    Java 技术产品和 API
    www.j2meforums.com
    优秀的中文网站:
    http://www.j2medev.com/
    http://www.j2me.com.cn/
    http://www.javame.cn/
    http://www.cnjm.net/

    http://www.j2mefans.com/

     

     

    一些实用的图形用户界面方法

     

    这个函数已反复应用于多个手机应用软件平台

    用法:参数定义:str——要分割的字符串
                            font——字体
                            rowMaxW——分割后每行宽度
             支持标示符:
             \n    换行
             \t     插入两个汉字长度的空格

    public static final String[] clipString(String str,Font font,int rowMaxW){
      if(str == null)
       return null;
      if(rowMaxW < font.charWidth('哈'))
       rowMaxW = font.charWidth('哈');         
      int strID = 0;
      int rowW = 0;
      Vector strManager = new Vector();
      char ch = ' ';
      while(str.length() > strID){
       ch = str.charAt(strID);
       switch(ch)
       {
       case '\n':
        strManager.addElement(str.substring(0,strID));
        str = str.substring(strID+1);
        rowW = 0;
        strID = 0;
        break;
       case '\t':
        StringBuffer sb = new StringBuffer(str);
        sb.deleteCharAt(strID);
        sb.insert(strID,"       ");
        str = sb.toString();
        break;
       default:
        if(rowW + font.charWidth(ch) > rowMaxW){
         strManager.addElement(str.substring(0,strID));
         str = str.substring(strID);
         rowW = 0;
         strID = 0;
        }else{
         rowW += font.charWidth(ch);
         strID++;
        }
       }
      }
      strManager.addElement(str);
      String[] o_Str = new String[strManager.size()];
      strManager.copyInto(o_Str);
      return o_Str;
    }



    返回结果是一个已切割好的String数组,只要用一个循环打印出来就可以了
    public static final void drawClipString(Graphics g,String[] clipStr,Font font,int color,int x,int y){
      if(clipStr == null){
         System.out.println("drawClipString");
         return;
      }
      int FONTH = font.getHeight();
      g.setFont(font);
      g.setColor(color);
      for(int i=0;i<clipStr.length;i++)
         g.drawString(clipStr[ i ],x,y+i*FONTH,0);
    }

    参数定义:clipStr——先前分割好的字符串数组
                   font——字体
                   color——颜色
                   x,y——打印的屏幕位置

    注意,切割和打印函数的字体参数必须保持一致!

    半透明技术(限MIDP2.0)

    // 获得半透明图片,透明度从0到10共分为11个等级
    public static final Image alfImage(Image img,int alf){
      if(img == null){
       System.out.println("alfImage");
       return null;
      }
      if(alf < 0)
       alf = 0;
      else if(alf > 10)
       alf = 10;
      int imgW = img.getWidth();
      int imgH = img.getHeight();
      int[] RGBData = new int[imgW*imgH];
      img.getRGB(RGBData,0,imgW,0,0,imgW,imgH);
      int tmp = ((alf*255/10) << 24)|0x00ffffff;
      for(int i=0;i<RGBData.length;i++)
       RGBData &= tmp;
      Image o_Img = Image.createRGBImage(RGBData,imgW,imgH,true);
      return o_Img;
    }

    灰度图转化函数// 得到灰度图
    public static final Image grayImage(Image img){
      if(img == null){
       System.out.println("grayAlfImage");
       return null;
      }
      int imgW = img.getWidth();
      int imgH = img.getHeight();
      int[] imgRGBData = new int[imgW*imgH];
      img.getRGB(imgRGBData,0,imgW,0,0,imgW,imgH);
      int ALF = 0;
      int R = 0;
      int G = 0;
      int B = 0;
      int GRAY = 0;
      for(int i=0;i<imgRGBData.length;i++){
       ALF = (imgRGBData >> 24) & 0xFF;
       R = (imgRGBData >> 16) & 0xFF;
       G = (imgRGBData >> 8) & 0xFF;
       B = imgRGBData & 0xFF;
       GRAY = (R*77+G*151+B*28 + 128)>>8;
       imgRGBData = (ALF<<24)|(GRAY<<16)|(GRAY<<8)|GRAY;
      }
      return Image.createRGBImage(imgRGBData,imgW,imgH,true);
    }

     

     

    图片透明效果


    public class ShadowMIDlet extends MIDlet {
    Canvas c = new ShadowCanvas();
    public ShadowMIDlet() {
    }
    protected void startApp() throws MIDletStateChangeException {
    Display.getDisplay(this).setCurrent(c);
    }
    protected void pauseApp() {
    // TODO Auto-generated method stub
    }
    protected void destroyApp(boolean arg0) throws MIDletStateChangeException {
    // TODO Auto-generated method stub
    }
    }
    /**
    *
    * @author Jagie
    *
    */
    class ShadowCanvas extends Canvas implements Runnable {
    int w, h;
    // 原始图片
    Image srcImage;
    // 原始图片的像素数组
    int[] srcRgbImage;
    // 渐变图片的像素数组
    int[] shadowRgbImage;
    int imgWidth, imgHeight;
    int count;
    public ShadowCanvas() {
    w = this.getWidth();
    h = this.getHeight();
    try {
    srcImage = Image.createImage("/av.png");
    } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    imgWidth = srcImage.getWidth();
    imgHeight = srcImage.getHeight();
    // 制造原始图片的像素数组,用一个int来代表每一个像素,按位表示方式是:0xAARRGGBB
    srcRgbImage = new int[imgWidth * imgHeight];
    // 获取原始图片的所有像素,参见MIDP APPI文档
    srcImage.getRGB(srcRgbImage, 0, imgWidth, 0, 0, imgWidth, imgHeight);
    shadowRgbImage = new int[srcRgbImage.length];
    System.arraycopy(srcRgbImage, 0, shadowRgbImage, 0,
    shadowRgbImage.length);
    // 渐变图片的所有像素已开始都是全透明的
    for (int i = 0; i < shadowRgbImage.length; i++) {
    shadowRgbImage &= 0x00ffffff;
    }
    new Thread(this).start();
    }
    public void paint(Graphics g) {
    g.setColor(0, 0, 0);
    g.fillRect(0, 0, w, h);
    // 绘制渐变图片
    g.drawRGB(shadowRgbImage, 0, imgWidth, (w - imgWidth) / 2,
    (h - imgHeight) / 2, imgWidth, imgHeight, true);
    g.setColor(0, 255, 0);
    g.drawString("count=" + count, w / 2, 30, Graphics.HCENTER
    | Graphics.TOP);
    }
    public void run() {
    while (true) {
    boolean changed = false;
    // 改变渐变图片的每一个像素
    for (int i = 0; i < shadowRgbImage.length; i++) {
    // 获取渐变图片的某一像素的alpha值
    int alpha = (shadowRgbImage & 0xff000000) >>> 24;
    // 原始图片的对应像素的alpha值
    int oldAlpha = (srcRgbImage & 0xff000000) >>> 24;
    if (alpha < oldAlpha) {
    // alpha值++
    shadowRgbImage = ((alpha + 1) << 24) | (shadowRgbImage & 0x00ffffff);
    changed = true;
    }
    }
    try {
    Thread.sleep(10);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    count++;
    repaint();
    // 当所有像素的alpha值都达到原始值后,线程运行结束
    if (!changed) {
    System.out.println("over");
    break;
    }
    }
    }
    }

     

     

     

    常见属性(Property)及其作用列表

    在J2ME开发中,我们经常需要和手机系统进行交互,获得一些和系统相关的信息,在J2ME API设计中,提供了一系列的系统属性,可以让我们来进行获得,下面就一一进行介绍。
    表1   CLDC、MIDP和JTWI属性
    属性名称
    属性作用
    microedition.profiles
    代表手机支持的MIDP版本,返回格式值为“MIDP-1.0”或“MIDP-2.0”
    microedition.configuration
    代表手机支持的CLDC版本,返回格式值为“CLDC-1.0”或“CLDC-2.0”
    microedition.locale
    代表手机所在的国家或地区,返回值格式为“en-US”
    microedition.platform
    代表手机的品牌和型号,Nokia手机的返回值格式为“Nokia6310i/4.42
    microedition.encoding
    代表手机默认的字符集名称,返回值格式为“ISO-8859-1”
    microedition.commports
    代表手机可以使用的串口列表,返回值中各个串口之间使用逗号分隔
    microedition.hostname
    MIDP2.0定义,代表本地主机名称,需要手机支持。
    microedition.jtwi.version
    代表手机支持的JTWI版本,值必须是“1.0”
     
    表2 可选包属性
    属性名称
    属性作用
    microedition.media.version
    代表手机支持的MMAPI版本,如果不支持则返回null
    microedition.pim.version
    代表手机支持的PIM API版本,如果不支持则返回null
    microedition.m3g.version
    代表手机支持的M3G API版本,如果不支持则返回null
    microedition.location.version
    代表手机支持的Location API版本,如果不支持则返回null
    Bluetooth.api.version
    代表手机支持的BT API版本,如果不支持则返回null
    microedition.io.file.
    FileConnection.version
    代表手机支持的FC API版本,如果不支持则返回null
    microedition.global.version
    代表手机支持的Mobile Internationalization API(JSR-238)版本,如果不支持则返回null
    microedition.chapi.version
    代表手机支持的CH(Content Handler) API(JSR211)版本,如果不支持则返回null
    microedition.sip.version
    代表手机支持的SIP API版本,如果不支持则返回null
     
    表3 MMAPI属性
    属性名称
    属性作用
    supports.mixing
    代表手机是否支持混音(同时播放多个Player),返回值为“true”或“false”
    supports.audio.capture
    代表手机是否支持声音捕获(录音),返回值为“true”或“false”
    supports.video.capture
    代表手机是否支持视频捕获(录像),返回值为“true”或“false”
    supports.recording
    代表手机是否支持记录(record),返回值为“true”或“false”
    audio.encodings
    代表手机支持的声音格式,返回值格式为“encoding=audio/wav”,多个格式之间使用至少一个空格进行间隔
    video.encodings
    代表手机支持的视频格式,返回值格式为“encoding=video/3gpp”,多个格式之间使用至少一个空格进行间隔
    video.snapshot.encodings
    代表手机使用getSnapshot方法获得的视频快照格式,返回值格式为“encoding=png”,多个格式之间使用至少一个空格进行间隔
    streamable.contents
    代表手机支持的流媒体格式,返回null代表不支持
     
    表4 Wireless Messaging API属性
    属性名称
    属性作用
    wireless.messaging.sms.smsc
    代表手机发送短信时的短信服务中心号码
     
    表5 FileConnection API
    属性名称
    属性作用
    fileconn.dir.photos
    代表手机中存储照片和其它图片的目录,例如“file:///c:/My files/ Images /”
    fileconn.dir.videos
    代表手机中存储视频的目录,例如“file:///c:/My files/Video clips/”
    fileconn.dir.tones
    代表手机中存储声音的目录,例如“file:///c:/My files/Tones/”
    fileconn.dir.memorycard
    代表手机中存储卡的根目录。例如“file:///d:/”
    fileconn.dir.private
    (Nokia S40不支持) 
    代表手机中MIDlet的私有工作目录,例如“file:///c:/System/MIDlets/[1015f294]/scratch”
    fileconn.dir.photos.name
    代表手机中图片目录的名称,例如“Images”
    fileconn.dir.videos.name
    代表手机中视频目录的名称,例如“Video clips”
    fileconn.dir.tones.name
    代表手机中声音目录的名称,例如“Sound clips”
    file.separator
    代表手机中的文件分隔符,例如“/”
    fileconn.dir.memorycard.name
    代表手机中存储卡的名称,例如“Memory card”
     
             使用这些属性,可以获得在程序运行过程中需要的很多和系统相关的信息,也可以使用表2中的属性来获得手机是否支持对应的可选包等信息。
             实际使用示例:
                       String name = System.getProperty(“microedition.platform”);
     
             注意:如果需要获得JVM或jad文件中的信息,需要使用MIDlet类中的getAppProperty方法,其属性名则需要查阅jad文件的设定,和本文所述的属性名无关。
     
    February 21

    数据库和 MIDP,第 5 部分:搜索记录存储

    在本系列文章的第四部分中,您学会如何遍历一个记录存储,按照有用的次序排序记录,以及使用过滤器选择期望的记录。本文探索各种用于发现符合指定准则的一个或多个记录的策略。

    搜索策略

    很明显,搜索一个特定记录或者记录集合的最简单方式是使用过滤器。该过滤器需要知道数据是如何存储在一个记录中的,因此您将希望在任何可能的情况下重用您的数据映射类。例如,在第 3 部分中我们定义 FieldListFieldBasedStore 类来处理读写任意数据到一个记录存储。进行一下重构,我们可以将不是特定于记录存储的代码移动到一个新的基类,FieldBasedRecordMapper

    package j2me.rms;
    
    import java.io.*;
    import javax.microedition.rms.*;
    import j2me.io.*;
    
    // A base class for writing and reading arbitrary
    // data as defined by a FieldList
    
    public abstract class FieldBasedRecordMapper {
    
        // Some useful constants
    
        public static Boolean TRUE = new Boolean( true );
        public static Boolean FALSE = new Boolean( false );
    
        // Markers for the types of string we support
    
        private static final byte NULL_STRING_MARKER = 0;
        private static final byte UTF_STRING_MARKER = 1;
    
        // Constructs the mapper for the given list
    
        protected FieldBasedRecordMapper(){
        }
    
        // Prepares for input by setting the data buffer.
    
        protected void prepareForInput( byte[] data ){
            if( _bin == null ){
                _bin = new DirectByteArrayInputStream( data );
                _din = new DataInputStream( _bin );
            } else {
                _bin.setByteArray( data );
            }
        }
    
        // Prepares the store for output. The streams are reused.
    
        protected void prepareForOutput(){
            if( _bout == null ){
                _bout = new DirectByteArrayOutputStream();
                _dout = new DataOutputStream( _bout );
            } else {
                _bout.reset();
            }
        }
    
        // Reads a field from the buffer.
    
        protected Object readField( int type ) throws IOException {
            switch( type ){
                case FieldList.TYPE_BOOLEAN:
                    return _din.readBoolean() ? TRUE : FALSE;
                case FieldList.TYPE_BYTE:
                    return new Byte( _din.readByte() );
                case FieldList.TYPE_CHAR:
                    return new Character( _din.readChar() );
                case FieldList.TYPE_SHORT:
                    return new Short( _din.readShort() );
                case FieldList.TYPE_INT:
                    return new Integer( _din.readInt() );
                case FieldList.TYPE_LONG:
                    return new Long( _din.readLong() );
                case FieldList.TYPE_STRING: {
                    byte marker = _din.readByte();
                    if( marker == UTF_STRING_MARKER ){
                        return _din.readUTF();
                    }
                }
            }
    
            return null;
        }
    
        // Converts an object to a boolean value.
    
        public static boolean toBoolean( Object value ){
            if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue();
            } else if( value != null ){
                String str = value.toString().trim();
    
                if( str.equals( "true" ) ) return true;
                if( str.equals( "false" ) ) return false;
    
                return( toInt( value ) != 0 );
            }
    
            return false;
        }
    
        // Converts an object to a char.
    
        public static char toChar( Object value ){
            if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value != null ){
                String s = value.toString();
                if( s.length() > 0 ){
                    return s.charAt( 0 );
                }
            }
    
            return 0;
        }
    
        // Converts an object to an int. This code
        // would be much simpler if the CLDC supported
        // the java.lang.Number class.
    
        public static int toInt( Object value ){
            if( value instanceof Integer ){
                return ((Integer) value).intValue();
            } else if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue() ? 1 : 0;
            } else if( value instanceof Byte ){
                return ((Byte) value).byteValue();
            } else if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value instanceof Short ){
                return ((Short) value).shortValue();
            } else if( value instanceof Long ){
                return (int) ((Long) value).longValue();
            } else if( value != null ){
                try {
                    return Integer.parseInt( value.toString() );
                }
                catch( NumberFormatException e ){
                }
            }
    
            return 0;
        }
    
        // Converts an object to a long. This code
        // would be much simpler if the CLDC supported
        // the java.lang.Number class.
    
        public static long toLong( Object value ){
            if( value instanceof Integer ){
                return ((Integer) value).longValue();
            } else if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue() ? 1 : 0;
            } else if( value instanceof Byte ){
                return ((Byte) value).byteValue();
            } else if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value instanceof Short ){
                return ((Short) value).shortValue();
            } else if( value instanceof Long ){
                return ((Long) value).longValue();
            } else if( value != null ){
                try {
                    return Long.parseLong( value.toString() );
                }
                catch( NumberFormatException e ){
                }
            }
    
            return 0;
        }
    
        // Writes a field to the output buffer.
    
        protected void writeField( int type, Object value )
                                     throws IOException {
            switch( type ){
                case FieldList.TYPE_BOOLEAN:
                    _dout.writeBoolean( toBoolean( value ) );
                    break;
                case FieldList.TYPE_BYTE:
                    _dout.write( (byte) toInt( value ) );
                    break;
                case FieldList.TYPE_CHAR:
                    _dout.writeChar( toChar( value ) );
                    break;
                case FieldList.TYPE_SHORT:
                    _dout.writeShort( (short) toInt( value ) );
                    break;
                case FieldList.TYPE_INT:
                    _dout.writeInt( toInt( value ) );
                    break;
                case FieldList.TYPE_LONG:
                    _dout.writeLong( toLong( value ) );
                    break;
                case FieldList.TYPE_STRING:
                    if( value != null ){
                        String str = value.toString();
                        _dout.writeByte( UTF_STRING_MARKER );
                        _dout.writeUTF( str );
                    } else {
                        _dout.writeByte( NULL_STRING_MARKER );
                    }
                    break;
            }
        }
    
        // Writes a set of fields to the output stream.
    
        protected byte[] writeStream( FieldList list, 
                                      Object[] fields )
                                           throws IOException {
            int count = list.getFieldCount();
            int len = ( fields != null ? fields.length : 0 );
    
            prepareForOutput();
    
            for( int i = 0; i < count; ++i ){
                writeField( list.getFieldType( i ),
                            ( i < len ? fields[i] : null ) );
            }
    
            return _bout.getByteArray();
        }
    
        private DirectByteArrayInputStream  _bin;
        private DirectByteArrayOutputStream _bout;
        private DataInputStream             _din;
        private DataOutputStream            _dout;
    }
    

    现在,我们扩展 FieldBasedRecordMapper 以创建另一个抽象类,FieldBasedFilter,这是我们过滤器的基类:

    package j2me.rms;
    
    import javax.microedition.rms.*;
    
    // A record filter for filtering records whose data
    // is mapped to a field list. The actual filter will
    // extend this class and implement the matchFields
    // method appropriately.
    
    public abstract class FieldBasedFilter
                          extends FieldBasedRecordMapper
                          implements RecordFilter {
    
        // Constructs the filter. The optional byte
        // array is an array that we want ignored,
        // usually the first record in the record store
        // where we store the field information.
    
        protected FieldBasedFilter(){
            this( null );
        }
    
        protected FieldBasedFilter( byte[] ignore ){
            _ignore = ignore;
        }
    
        // Compares two byte arrays.
    
        private boolean equal( byte[] a1, byte[] a2 ){
            int len = a1.length;
    
            if( len != a2.length ) return false;
    
            for( int i = 0; i < len; ++i ){
                if( a1[i] != a2[i] ) return false;
            }
    
            return true;
        }
    
        // Called to filter a record.
    
        public boolean matches( byte[] data ){
            if( _ignore != null ){
                if( equal( _ignore, data ) ) return false;
            }
    
            prepareForInput( data );
            return matchFields();
        }
    
        // The actual filter implements this method.
    
        protected abstract boolean matchFields();
    
        private byte[] _ignore;
    }
    

    假设我们这样定义一个如下的 FieldList 实例:

    ...
    FieldList empFields = new FieldList( 5 );
    
    empFields.setFieldType( 0, FieldList.TYPE_INT );
    empFields.setFieldName( 0, "ID" );
    empFields.setFieldType( 1, FieldList.TYPE_STRING );
    empFields.setFieldName( 1, "Given Name" );
    empFields.setFieldType( 2, FieldList.TYPE_STRING );
    empFields.setFieldName( 2, "Last Name" );
    empFields.setFieldType( 3, FieldList.TYPE_BOOLEAN );
    empFields.setFieldName( 3, "Active" );
    empFields.setFieldType( 4, FieldList.TYPE_CHAR );
    empFields.setFieldName( 4, "Sex" );
    ...
    

    一个匹配特定姓氏的过滤器看起来类似于:

    package j2me.rms;
    
    import java.io.IOException;
    
    // A filter that matches a specific last name
    // in an employee record.
    
    public class MatchLastName extends FieldBasedFilter {
        public MatchLastName( String name ){
            this( name, null );
        }
    
        public MatchLastName( String name, byte[] ignore ){
            super( ignore );
            _name = name;
        }
    
        protected boolean matchFields(){
            try {
                readField( FieldList.TYPE_INT );
                readField( FieldList.TYPE_STRING );
    
                String ln = (String)
                           readField( FieldList.TYPE_STRING );
    
                return ln.equals( _name );
            }
            catch( IOException e ){
                return false;
            }
        }
    
        private String _name;
    }
    

    发现匹配的记录是一件简单的事情:

    ...
    RecordStore employees = ... // list of employees
    RecordFilter lname = new MatchLastName( "Smith" );
    RecordEnumeration enum =
          employees.enumerateRecords( lname, null, false );
    
    while( enum.hasNextElement() ){
        int id = enum.nextRecordId();
        ... // etc. etc./
    }
    
    enum.destroy();
    ...
    

    仔细地编码,您可以避免打开任何不需要的记录。您可以采取的一个方法是在内存中缓存最近访问的记录。例如,每当过滤器匹配一个记录时,打开该记录并且将它的已打开形式存储到缓存中,这种打开形式通常是一个表示单一实体的对象,例如雇员。将它的记录 ID 用作关键字 —— 当然,您需要在记录中存储 ID。当您遍历该枚举时,在访问底层记录存储之前检查已打开记录的缓存。

    实际上,使用枚举方式将匹配记录收集和打开为单独的列表可能更为简单。考虑我们在第 2 部分中定义的 Contact 类,它的简单的 toByteArray()fromByteArray() 方法用于在实例和字节数组之间进行映射:

    package j2me.example;
    
    import java.io.*;
    
    // The contact information for a person
    
    public class Contact {
        private String _firstName;
        private String _lastName;
        private String _phoneNumber;
    
        public Contact(){
        }
    
        public Contact( String firstName, String lastName,
                        String phoneNumber )
        {
            _firstName = firstName;
            _lastName = lastName;
            _phoneNumber = phoneNumber;
        }
    
        public String getFirstName(){
            return _firstName != null ? _firstName : "";
        }
    
        public String getLastName(){
            return _lastName != null ? _lastName : "";
        }
    
        public String getPhoneNumber(){
            return _phoneNumber != null ? _phoneNumber : "";
        }
    
        public void setFirstName( String name ){
            _firstName = name;
        }
    
        public void setLastName( String name ){
            _lastName = name;
        }
    
        public void setPhoneNumber( String number ){
            _phoneNumber = number;
        }
    
        public void fromByteArray( byte[] data ) 
                                    throws IOException {
            ByteArrayInputStream bin = 
                            new ByteArrayInputStream( data );
            DataInputStream din = new DataInputStream( bin );
    
            fromDataStream( din );
            din.close();
        }
    
        public byte[] toByteArray() throws IOException {
            ByteArrayOutputStream bout = 
                            new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream( bout );
    
            toDataStream( dout );
            dout.close();
    
            return bout.toByteArray();
        }
    
        public void fromDataStream( DataInputStream din ) 
                                    throws IOException {
            _firstName = din.readUTF();
            _lastName = din.readUTF();
            _phoneNumber = din.readUTF();
        }
    
        public void toDataStream( DataOutputStream dout ) 
                                  throws IOException {
            dout.writeUTF( getFirstName() );
            dout.writeUTF( getLastName() );
            dout.writeUTF( getPhoneNumber() );
        }
    }
    

    下面的类使用记录过滤器作为一种发现和存储匹配记录的方式。它返回 false 以指示一个空的枚举,从而立即被丢弃:

    package j2me.example;
    
    import java.io.*;
    import java.util.*;
    import javax.microedition.rms.*;
    
    // Finds the contacts whose first and/or last
    // names match the given values.
    
    public class FindContacts {
    
        // Constructs the finder for the given names. If
        // both names are non-null, both names must match,
        // otherwise only the given name needs to match.
    
        public FindContacts( String fname, String lname ){
            _fname = normalize( fname );
            _lname = normalize( lname );
        }
    
        // Traverses the data in the record store and
        // returns a list of matching Contact objects.
    
        public Vector list( RecordStore rs )
                            throws RecordStoreException,
                                   IOException {
    
            Vector v = new Vector();
            Filter f = new Filter( v );
            RecordEnumeration enum =
                   rs.enumerateRecords( f, null, false );
    
            // The enum will never have any elements in it,
            // but we call this to force it to traverse
            // its list.
    
            enum.hasNextElement();
            enum.destroy();
    
            return v;
        }
    
        // Returns whether or not a given Contact
        // instance matches our criteria.
    
        public boolean matchesContact( Contact c ){
            boolean sameFirst = false;
            boolean sameLast = false;
    
            if( _fname != null ){
                sameFirst =
                   c.getFirstName().toLowerCase().equals(_fname);
            }
    
            if( _lname != null ){
                sameLast =
                   c.getLastName().toLowerCase().equals( _lname );
            }
    
            if( _fname != null && _lname != null ){
                return sameFirst && sameLast;
            }
    
            return sameFirst || sameLast;
        }
    
        // Normalize our name data
    
        private static String normalize( String name ){
            return( name != null ? 
                    name.trim().toLowerCase() : null );
        }
    
        private String _fname;
        private String _lname;
    
        // A record filter that always returns false but
        // whenever it finds a matching contact it adds it
        // to the given list.
    
        private class Filter implements RecordFilter {
            private Filter( Vector list ){
                _list = list;
            }
    
            public boolean matches( byte[] data ){
                try {
                    Contact c = new Contact();
                    c.fromByteArray( data );
    
                    if( matchesContact( c ) ){
                        _list.addElement( c );
                    }
                }
                catch( IOException e ){
                }
    
                return false;
            }
    
            private Vector _list;
        }
    }
    

    不幸的是,内存限制可能阻止每次缓存更多的对象。您可以通过使用索引搜索一张单独维护的表,从而获得某些性能。这个表将记录 ID 与关键字值例如联系名相配对。一个索引通常足够的小,以便于将它保持在内存中,并且省去您每次需要发现特定记录时都要使用枚举的麻烦。  

    关于作者

    Eric Giguere 是 iAnywhere Solutions 的一名软件开发人员,iAnywhere Solutions 是 Sybase 的一个子公司,他在那里从事关于手持设备和无线计算的 Java 技术。他拥有 Waterloo 大学计算机科学的学士和硕士学位,并广泛撰写有关计算主题的文章。

    数据库和 MIDP,第 4 部分:过滤和遍历策略

    在本系列文章的第 2 和第 3 部分,我们讨论在数据和字节数组之间相互映射的基本方式,从而在记录存储中存储它们,这些记录存储是通过记录管理系统(Record Management System,RMS)管理的。读写数据总是需要克服的第一个障碍,但是发现您所需要的数据也同样的重要,并且这样做的话,您需要能够浏览记录存储,以一种有用的方式排序记录,以及使用过滤器提取出想要的数据。本文探索执行这些任务的不同策略;第 5 部分将建立在这里所学知识的基础之上,并且向您展示如何搜索满足指定规则的记录。

    记录 ID 并不是索引

    为了读或写一条记录,您需要知道它的记录 ID。在第 1 部分中,您了解到记录 ID 是一个整数值,它唯一地标识在记录存储中的一条记录 - 并且它不是记录存储中的索引。这个差异具有某些重要的含义。

    如果具有 N 个记录的存储被索引,那么每条记录都将具有一个索引,其范围是 0 到 N-1 或者 1 到 N,这取决于范围是从 0 还是从 1 开始的。每当一条记录被删除或者插入的时候,在存储中的位于后面的记录索引将会相应的改变。该范围将会收缩或者增长,但是保持连续。

    与索引不同的是,在记录存储中,无论在一条记录之前插入或者去除多少其他记录,该记录 ID 并不改变。增加到记录存储中第一条记录将其记录 ID 赋值为 1,下一条记录 ID 为 2,等等。如果您删除一条记录,它的记录 ID 变为无效,并且任何访问该记录的企图都会抛出 InvalidRecordIDException。无效的记录 ID 不会保持连续。

    由于它们唯一地标识记录,因此您可以通过将一个记录的 ID 存储为另一条记录中的数据值,从而使用记录 ID 将两个或者更多记录连接到一起。您还可以使用记录 ID 来同步数据和外部应用程序,正如您将在第 6 部分中所看到的。

    记录 ID 的主要缺点是它们使记录存储遍历变得复杂化;您不能像在数组中那样,在一个索引集合中进行迭代。您必须使用两种遍历技术之一:强制或者枚举。

     

    强制遍历

    利用强制方法,您只是简单地逐个提取记录,从第一条记录开始,忽略无效的记录,一直继续直到您已经获取所有的记录:

    ...
    RecordStore rs = ... // an open record store
    int lastID = rs.getNextRecordID();
    int numRecords = rs.getNumRecords();
    int count = 0;
    
    for( int id = 1; 
         id < lastID && count < numRecords;
         ++id ){
        try {
            byte[] data = rs.getRecord( id );
            ... // process the data
            ++count;
        }
        catch( InvalidRecordIDException e ){
            // just ignore and move to the next record
        }
        catch( RecordStoreException e ){
            // a more general error that should be handled
            // somehow
            break;
        }
    }
    ...
    

    该代码调用 getNextRecordID() 来发现将要增加到存储的下一条记录的 ID,并且将它用作可能的记录标识符的上界。该代码在每次读取到一个有效值时都增加计数器,因此一旦已经看到所有记录,就可以停止遍历。注意,在遍历期间,记录存储没有被锁定 —— 如果它对于您很重要的话,您将需要使用一个 synchronized 块来防止其他线程改变该记录。

    强制方法易于理解,并且如果缺失记录很少的话,将会工作得很好,但是首选的方法是使用枚举。 

    枚举记录

    并不是检查每个记录 ID 以查看哪个记录是有效的,这本质上是强制方法所做的,您可以使用 RecordEnumeration 接口,请求 RMS 为您返回有效记录 ID 的一个枚举。该接口并没有扩展标准的 java.util.Enumeration 接口,但是它以一种类似的方式工作。事实上,它实际上是一个更有能力的接口:您可以向后或者向前遍历一个记录枚举,并且当记录发生改变时可以使用它来跟踪变化。

    您可以针对希望遍历的记录存储调用 enumerateRecords() 方法,从而获取一个枚举,正如在该实例中:

    ...
    RecordStore rs = ... // an open record store
    RecordEnumeration enum = 
                  rs.enumerateRecords( null, null, false );
    
    ... // use the enumeration here
    
    enum.destroy(); // always clean it up!
    ...
    

    出于简单性考虑,异常处理已经从该代码片断中被忽略了,但是应该意识到 enumerateRecords() 可以抛出一个 RecordStoreNotOpenException。

    enumerateRecords() 的前面两个参数控制如何过滤和排序记录 —— 我们将简要地讨论它们。第三个参数控制枚举是否跟踪记录存储的变化。跟踪变化要求额外的耗费,并且在大多数情况下并不需要,因此我们将在所有的实例中将它设置为 false。

    当您已经处理完一个枚举,您必须调用destroy() 方法,从而释放系统已经分配给它的任何资源 —— 记住,在 CLDC 中没有对象终止。未销毁的枚举将会导致您的应用程序泄漏内存。

    使用 hasNextElement() and nextRecordId() 在枚举中向前移动:

    ...
    RecordStore rs = ...
    RecordEnumeration enum = ...
    
    try {
        while( enum.hasNextElement() ){
            int id = enum.nextRecordId();
            byte[] data = rs.getRecord( id );
            ... // do something here
        }
    }
    catch( RecordStoreException e ){
        // handle the error here
    }
    ...
    

    您还可以使用 hasPreviousElement()previousRecordId() 向后移动。注意,如果一条记录从记录存储中被删除,同时该枚举是活动的,那么 nextRecordId()previousRecordId() 都将抛出 InvalidRecordIDException。

    一个未排序枚举返回其记录的顺序是特定于实现,因此不要基于该顺序做出任何假设。

    出于方便性考虑,您可以使用 nextRecord() 或者 previousRecord() 以获取后面或者前面记录的数据,而不是它的记录 ID:

    ...
    try {
        while( enum.hasNextElement() ){
            byte[] data = enum.nextRecord();
            ... // do something here
        }
    }
    catch( RecordStoreException e ){
        // handle the error here
    }
    ...
    

    然而,您将不会知道正在讨论的记录 ID,或者不能直接将数据读取到您已经分配的字节数组中。然而,如果您正在过滤或者排序枚举,数据可能被缓存在内存中,因此从枚举中获取它可能比从底层的记录存储获取更为有效。

    一个对于 numRecords() 的调用将向您提供枚举中的记录 ID,但是该调用将导致该枚举立即具体化而不是递增地具体化,并且该操作可能使用大量内存或者在处理中导致一个显而易见的停顿。

    使用 reset() 将一个枚举重新设置为它的初始状态,使用 rebuild() 强制它根据记录存储的当前状态更新它自身。

    使用 isKeptUpdated() 来检查一个枚举是否跟踪对于记录存储的变化,使用 keepUpdated() 以改变它的跟踪状态。  

    过滤枚举记录

    只是对于在记录存储中的数据子集感兴趣吗?您可以通过使用一个过滤器,从而具有一个忽略不必要记录的枚举,该过滤器实现 RecordFilter 接口。过滤器的 matches() 方法应该指示是否一条记录将被包括在该枚举中:

    public class MyFilter implements RecordFilter {
        public boolean matches( byte[] recordData ){
            ... // matching code here
        }
    }
    

    理想情况是,您需要过滤的信息是在数组的开头 —— 您希望尽可能快地确定一个匹配。为了使用一个过滤器,将它作为第一个参数传递给 enumerateRecords()

    ...
    enum = rs.enumerateRecords( new MyFilter(), null, false );
    ...
    

    记录 ID 并不传递给过滤器,只有字节数组包括记录数据。如果您必须知道正被匹配的是哪个记录 ID,您将需要在该记录中存储它自己的 ID,或者使用某些其他的方式从记录的数据中标识该记录。 例如,在第 3 部分中我们使用记录存储中的第一条记录,用于保持其余记录中的域的有关信息 —— 很明显,您希望将该记录从任何枚举中过滤出去。  

    排序枚举记录

    为了保证记录以一种一致的、可预测的次序被返回,您必须排序该枚举。您向该枚举提供一个比较器,这是一个实现 RecordComparator 接口的对象。比较器的 compare() 方法返回一条记录是否先于、等于或者后于其他记录:

    public class MyComparator implements RecordComparator {
        public int compare( byte[] r1, byte[] r2 ){
            int salary1 = getSalary( r1 );
            int salary2 = getSalary( r2 );
    
            if( salary1 < salary2 ){
                return PRECEDES;
            } else if( salary1 == salary2 ){
                return EQUIVALENT;
            } else {
                return FOLLOWS;
            }
            }
    
            private int getSalary( byte[] data ){
                ... // code to get salary info
            }
        }
    

    注意常量 PRECEDESEQUIVALENT FOLLOWS 的使用。这些常量是通过 RecordComparator接口定义的。为了使用一个比较器,将它作为第二个参数传递给enumerateRecords():

    ...
    enum = rs.enumerateRecords( null, new MyComparator(), false );
    ...
    

    利用过滤器,没有记录标识符会被传递给比较器,只是原始的记录数据。

    当您创建枚举时,可以提供过滤器和比较器。如果您这样做的话,在数据被排序之前应用过滤器。

     

    下一步工作

    现在,我们知道如何移动记录存储,以及如何排序和过滤记录,您已经准备好学习第 5 部分,该部分将描述相关策略,用于搜索一条记录存储中符合特定准则的对象。  

    关于作者

    Eric Giguere 是 iAnywhere Solutions 的一名软件开发人员,iAnywhere Solutions 是 Sybase 的一个子公司,他在那里从事关于手持设备和无线计算的 Java 技术。他拥有 Waterloo 大学计算机科学的学士和硕士学位,并广泛撰写有关计算主题的文章。

    数据库和 MIDP,第三部分:使用数据映射

     

    本系列中的 第二部分 介绍了数据映射的基本知识。您学会了如何在字节数组(byte array)中存储原始数据类型的数值,如何使用流向记录库(record store)中存储对象和从记录库中检索对象以及如何从字节数组中抽取已存储的数值。在本文中,您将学会如何使您的应用程序远离这些低级操作,方法是对核心类进行扩展使其包含读写操作,创建并使用字段列表来存储和检索对象。

    扩展核心类

    J2ME 开发人员的目标之一是最小化应用程序对内存的使用。理想情况下,在任何给定的时间内,内存中应该只含有某个数据块的一个副本。然而,当您向 ByteArrayOutputStream 写数据时,您从来无法访问其基本的字节数组--调用 toByteArray() 返回该数组的一个拷贝。如果您紧接着就将该字节数组保存到记录库中,则会造成没必要的内存浪费。要直接访问该数组,只需要对类 ByteArrayOutputStream 进行简单的扩展即可,扩展后的类称为 DirectByteArrayOutputStream

    package j2me.io;
    
    import java.io.*;
    
    // A version of ByteArrayOutputStream that gives you
    // direct access to the underlying byte array if
    // you need it.
    
    public class DirectByteArrayOutputStream 
                           extends ByteArrayOutputStream {
    
        // Constructs a byte array output stream of default size
    
        public DirectByteArrayOutputStream(){
            super();
        }
    
        // Constructs a byte array output stream of given size
    
        public DirectByteArrayOutputStream( int size ){
            super( size );
        }
    
        // Returns a reference to the underlying byte array.
        // The actual amount of data in the byte array is
        // obtained via the size method.
    
        public synchronized byte[] getByteArray(){
            return buf;
        }
    
        // Swaps in a new byte array for the old one, resetting
        // the count as well.
    
        public synchronized byte[] swap( byte[] newBuf ){
            byte[] oldBuf = buf;
            buf = newBuf;
            reset();
            return oldBuf;
        }
    }
    

    记住调用 size() 方法查看存储在字节数组中的实际数据的多少:

    ...
    DirectByteArrayOutputStream bout = ... 
    RecordStore rs = ...
    
    int numBytes = bout.size();
    byte[] data = bout.getByteArray();
    
    rs.addRecord( data, 0, numBytes );
    ...
    

    未来保持一致性,您可以用类似的方法对 ByteArrayInputStream 类进行扩展:

    package j2me.io;
    
    import java.io.*;
    
    // A version of ByteArrayInputStream that lets you
    // replace the underlying byte array.
    
    public class DirectByteArrayInputStream 
                      extends ByteArrayInputStream {
    
        // Constructs an output stream from the given array
    
        public DirectByteArrayInputStream( byte buf[] ){
            super( buf );
        }
    
        // Constructs an output stream from the given subarray
    
        public DirectByteArrayInputStream( byte buf[],
                                           int offset, 
                                           int length ){
            super( buf, offset, length );
        }
    
        // Resets the array the stream reads from
    
        public synchronized void setByteArray( byte[] buf ){
            this.buf = buf;
            this.pos = 0;
            this.count = buf.length;
            this.mark = 0;
        }
    
        // Resets the array the stream reads from
    
        public synchronized void setByteArray( byte[] buf, 
                                               int offset,
                                               int length ){
            this.buf = buf;
            this.pos = offset;
            this.count = Math.min( offset + length, buf.length );
            this.mark = offset;
        }
    }
    

    注意类ByteArrayInputStream和类 ByteArrayOutputStream以及以上所写的两个扩展类都使用了用于线程安全的同步方法。尽管在大多数情况下只有单一线程使用这些流,同步也就没有必要了。如果您的应用程序需要读写大量的数据,可考虑创建基于这些类的非同步的版本以获得较快的速度。

    您也可以很容易的对 DataInputStreamDataOutputStream 进行扩展。例如,如果您需要在多个地方写整型数组,请使用 DataOutputStream 的以下扩展:

    package j2me.io;
    
    import java.io.*;
    
    public class ExtendedDataOutputStream extends DataOutputStream {
        public ExtendedDataOutputStream( OutputStream out ){
            super( out );
        }
    
        public final void writeIntArray( int[] arr ) 
                                         throws IOException {
            int size = arr.length;
            writeInt( size );
            for( int i = 0; i < size; ++i ){
                writeInt( arr[i] );
            }
        }
    }
    

    相反,您可以将这种代码放入一个帮助器类,因为您不需要访问任何的 protected members。

    创建记录字段

    现在您已经拥有了创建基于字段的记录库的所有工具,记录库中的每条记录都是一组指定类型的命名字段。您利用两个类来管理该记录库。其中的第一个类 FieldList 管理字段自身的信息--元数据:

    package j2me.rms;
    
    import java.io.*;
    import javax.microedition.rms.*;
    
    // Maintains information about the fields in a
    // field-based record store. Currently just a list of
    // field types and (optional) field names, but could
    // easily be expanded to store other information.
    
    public class FieldList {
    
        private static final int VERSION = 1;
    
        // The basic field types.
    
        public static final byte TYPE_BOOLEAN = 1;
        public static final byte TYPE_BYTE = 2;
        public static final byte TYPE_CHAR = 3;
        public static final byte TYPE_SHORT = 4;
        public static final byte TYPE_INT = 5;
        public static final byte TYPE_LONG = 6;
        public static final byte TYPE_STRING = 7;
    
        // Constructs an empty list.
    
        public FieldList(){
        }
    
        // Constructs a list of the given size.
    
        public FieldList( int numFields ){
            if( numFields < 0 || numFields > 255 ){
                throw new IllegalArgumentException( 
                           "Bad number of fields" );
            }
    
            _types = new byte[ numFields ];
            _names = new String[ numFields ];
        }
    
        // Returns the number of fields.
    
        public int getFieldCount(){
            return _types != null ? _types.length : 0;
        }
    
        // Returns the name of a field.
    
        public String getFieldName( int index ){
            String name = _names[ index ];
            return name != null ? name : "";
        }
    
        // Returns the type of a field.
    
        public byte getFieldType( int index ){
            return _types[ index ];
        }
    
        // Reads the field list from a byte array.
    
        public void fromByteArray( byte[] data )
                                   throws IOException {
            ByteArrayInputStream bin = 
                      new ByteArrayInputStream( data );
            fromDataStream( new DataInputStream( bin ) );
            bin.close();
        }
    
        // Reads the fields list from a data stream.
    
        public void fromDataStream( DataInputStream din )
                                    throws IOException {
            int version = din.readUnsignedByte();
            if( version != VERSION ){
                throw new IOException( "Incorrect version " +
                      version + " for FieldList, expected " +
                      VERSION );
            }
    
            int numFields = din.readUnsignedByte();
    
            _types = new byte[ numFields ];
            _names = new String[ numFields ];
    
            if( numFields > 0 ){
                din.readFully( _types );
        
                for( int i = 0; i < numFields; ++i ){
                    _names[i] = din.readUTF();
                }
            }
        }
    
        // Reads a field list from a record store.
    
        public void fromRecordStore( RecordStore rs, int index )
                                     throws IOException, 
                                            RecordStoreException {
            fromByteArray( rs.getRecord( index ) );
        }
    
        // Sets the name of a field.
    
        public void setFieldName( int index, String name ){
            _names[ index ] = name;
        }
    
        // Sets the type of a field.
    
        public void setFieldType( int index, byte type ){
            _types[ index ] = type;
        }
    
        // Stores the fields list to a byte array
    
        public byte[] toByteArray() throws IOException {
            ByteArrayOutputStream bout = 
                         new ByteArrayOutputStream();
            toDataStream( new DataOutputStream( bout ) );
            byte[] data = bout.toByteArray();
            bout.close();
            return data;
        }
    
        // Stores the fields list to a data stream
    
        public void toDataStream( DataOutputStream out )
                                  throws IOException {
            out.writeByte( VERSION );
    
            int count = getFieldCount();
    
            out.writeByte( count );
    
            if( count > 0 ){
                out.write( _types, 0, count );
    
                for( int i = 0; i < count; ++i ){
                    out.writeUTF( getFieldName( i ) );
                }
            }
        }
    
        // Writes a field list to a record store.
    
        public int toRecordStore( RecordStore rs, int index )
                                   throws IOException, 
                                          RecordStoreException {
            byte[]  data = toByteArray();
            boolean add = true;
    
            if( index > 0 ){
                try {
                    rs.setRecord( index, data, 0, data.length );
                    add = false;
                }
                catch( InvalidRecordIDException e ){
                }
            }
    
            // If the record doesn't actually exist yet,
            // go ahead and create it by inserting dummy
            // records ahead of it
    
            if( add ){
                synchronized( rs ){
                    int nextID = rs.getNextRecordID();
                    if( index <= 0 ) index = nextID;
        
                    while( nextID < index ){
                        rs.addRecord( null, 0, 0 );
                    }
        
                    if( nextID == index ){
                        rs.addRecord( data, 0, data.length );
                    }
                }
            }
    
            return index;
        }
    
        private String[] _names;
        private byte[]   _types;
    }
    

    实际上,一个 FieldList 实例只是两个数组的一个包装器。

    在其核心,一个 FieldList 实例只是两个数组的一个包装器。第一个数组存储每个字段的类型,第二个数组存储每个字段的名称。名称是可选的,重要的是类型,因为它决定如何将数据写入记录和从记录中读出。所有标准的 Java 原始数据类型都支持,还有 String 类型。下面是对存储某个组织的部门列表的字段的定义:

    ...
    FieldList depts = new FieldList( 3 );
    depts.setFieldType( 0, FieldList.TYPE_SHORT );
    depts.setFieldName( 0, "ID" );
    depts.setFieldType( 1, FieldList.TYPE_STRING );
    depts.setFieldName( 1, "Name" );
    depts.setFieldType( 2, FieldList.TYPE_INT );
    depts.setFieldName( 2, "ManagerID" );
    ...
    

    一个 FieldList

    实例可存储在一个数据流、一个字节数组或者一个记录库中。如果您将字段列表存储为记录库中的第一条记录,则任何可打开该记录库的代码就可以读取该记录以决定其余字段的字段布局。要使这个策略起到作用,您必须在创建记录库后立即存储记录列表:

    ...
    FieldList list = ... // a field list
    RecordStore rs = RecordStore.openRecordStore( "foo", true );
    if( rs.getNumRecords() == 0 ){ // empty, store it
        list.toRecordStore( rs, -1 );
    }
    ...
    

    toRecordStore() 方法的第二个参数确定了保存字段数据所使用的记录。负值表示将要添加一条新的记录。

    管理基于字段的记录库所需的第二个类是 FieldBasedStore,它将管理实际的读写操作:

    package j2me.rms;
    
    import java.io.*;
    import javax.microedition.rms.*;
    import j2me.io.*;
    
    // A wrapper class for a record store that allows the
    // records to be accessed as a set of fields. The field
    // definitions are maintained separately using a FieldList
    // object, which can be stored as part of the record store
    // or separately.
    
    public class FieldBasedStore {
    
        // Some useful constants
    
        public static Boolean TRUE = new Boolean( true );
        public static Boolean FALSE = new Boolean( false );
    
        // Markers for the types of string we support
    
        private static final byte NULL_STRING_MARKER = 0;
        private static final byte UTF_STRING_MARKER = 1;
    
        // Constructs a field store where the field list is
        // assumed to be stored in the first record.
    
        public FieldBasedStore( RecordStore rs )
                                throws IOException,
                                       RecordStoreException {
            this( rs, 1 );
        }
    
        // Constructs a field store where the field list is
        // stored in the given record.
    
        public FieldBasedStore( RecordStore rs, int fieldListID )
                                throws IOException,
                                       RecordStoreException {
            this( rs, loadFieldList( rs, fieldListID ) );
        }
    
        // Constructs a field store with the given field list.
    
        public FieldBasedStore( RecordStore rs, FieldList list ){
            _rs = rs;
            _fieldList = list;
        }
    
        // Adds a new record to the store. Returns the new
        // record ID.
    
        public synchronized int addRecord( Object[] fields )
                                     throws IOException,
                                            RecordStoreException {
            writeStream( fields );
            byte[] data = _bout.getByteArray();
            return _rs.addRecord( data, 0, data.length );
        }
    
        // Returns the current field list.
    
        public FieldList getFieldList(){
            return _fieldList;
        }
    
        // Returns the record store.
    
        public RecordStore getRecordStore(){
            return _rs;
        }
    
        // Loads the field list from the record store.
    
        private static FieldList loadFieldList( RecordStore rs,
                                                int fieldListID )
                                     throws IOException,
                                            RecordStoreException {
            FieldList list = new FieldList();
            list.fromRecordStore( rs, fieldListID );
            return list;
        }
    
        // Prepares the store for input by making sure that
        // the data buffer is big enough. The streams are
        // reused.
    
        private void prepareForInput( int size ){
            if( _buffer == null || _buffer.length < size ){
                _buffer = new byte[ size ];
            }
    
            if( _bin == null ){
                _bin = new DirectByteArrayInputStream( _buffer );
                _din = new DataInputStream( _bin );
            } else {
                _bin.setByteArray( _buffer );
            }
        }
    
        // Prepares the store for output. The streams are reused.
    
        private void prepareForOutput(){
            if( _bout == null ){
                _bout = new DirectByteArrayOutputStream();
                _dout = new DataOutputStream( _bout );
            } else {
                _bout.reset();
            }
        }
    
        // Reads a field from the buffer.
    
        private Object readField( int type ) throws IOException {
            switch( type ){
                case FieldList.TYPE_BOOLEAN:
                    return _din.readBoolean() ? TRUE : FALSE;
                case FieldList.TYPE_BYTE:
                    return new Byte( _din.readByte() );
                case FieldList.TYPE_CHAR:
                    return new Character( _din.readChar() );
                case FieldList.TYPE_SHORT:
                    return new Short( _din.readShort() );
                case FieldList.TYPE_INT:
                    return new Integer( _din.readInt() );
                case FieldList.TYPE_LONG:
                    return new Long( _din.readLong() );
                case FieldList.TYPE_STRING: {
                    byte marker = _din.readByte();
                    if( marker == UTF_STRING_MARKER ){
                        return _din.readUTF();
                    }
                }
            }
    
            return null;
        }
    
        // Reads the record at the given ID and returns it as
        // a set of objects that match the types in the 
        // field list.
    
        public synchronized Object[] readRecord( int recordID )
                                     throws IOException,
                                            RecordStoreException {
            prepareForInput( _rs.getRecordSize( recordID ) );
            _rs.getRecord( recordID, _buffer, 0 );
    
            int count = _fieldList.getFieldCount();
            Object[] fields = new Object[ count ];
    
            for( int i = 0; i < count; ++i ){
                fields[i] = readField(_fieldList.getFieldType(i));
            }
    
            return fields;
        }
    
        // Converts an object to a boolean value.
    
        public static boolean toBoolean( Object value ){
            if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue();
            } else if( value != null ){
                String str = value.toString().trim();
    
                if( str.equals( "true" ) ) return true;
                if( str.equals( "false" ) ) return false;
    
                return( toInt( value ) != 0 );
            }
    
            return false;
        }
    
        // Converts an object to a char.
    
        public static char toChar( Object value ){
            if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value != null ){
                String s = value.toString();
                if( s.length() > 0 ){
                    return s.charAt( 0 );
                }
            }
    
            return 0;
        }
    
        // Converts an object to an int. This code
        // would be much simpler if the CLDC supported
        // the java.lang.Number class.
    
        public static int toInt( Object value ){
            if( value instanceof Integer ){
                return ((Integer) value).intValue();
            } else if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue() ? 1 : 0;
            } else if( value instanceof Byte ){
                return ((Byte) value).byteValue();
            } else if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value instanceof Short ){
                return ((Short) value).shortValue();
            } else if( value instanceof Long ){
                return (int) ((Long) value).longValue();
            } else if( value != null ){
                try {
                    return Integer.parseInt( value.toString() );
                }
                catch( NumberFormatException e ){
                }
            }
    
            return 0;
        }
    
        // Converts an object to a long. This code
        // would be much simpler if the CLDC supported
        // the java.lang.Number class.
    
        public static long toLong( Object value ){
            if( value instanceof Integer ){
                return ((Integer) value).longValue();
            } else if( value instanceof Boolean ){
                return ((Boolean) value).booleanValue() ? 1 : 0;
            } else if( value instanceof Byte ){
                return ((Byte) value).byteValue();
            } else if( value instanceof Character ){
                return ((Character) value).charValue();
            } else if( value instanceof Short ){
                return ((Short) value).shortValue();
            } else if( value instanceof Long ){
                return ((Long) value).longValue();
            } else if( value != null ){
                try {
                    return Long.parseLong( value.toString() );
                }
                catch( NumberFormatException e ){
                }
            }
    
            return 0;
        }
    
        // Writes a field to the output buffer.
    
        private void writeField( int type, Object value )
                                     throws IOException {
            switch( type ){
                case FieldList.TYPE_BOOLEAN:
                    _dout.writeBoolean( toBoolean( value ) );
                    break;
                case FieldList.TYPE_BYTE:
                    _dout.write( (byte) toInt( value ) );
                    break;
                case FieldList.TYPE_CHAR:
                    _dout.writeChar( toChar( value ) );
                    break;
                case FieldList.TYPE_SHORT:
                    _dout.writeShort( (short) toInt( value ) );
                    break;
                case FieldList.TYPE_INT:
                    _dout.writeInt( toInt( value ) );
                    break;
                case FieldList.TYPE_LONG:
                    _dout.writeLong( toLong( value ) );
                    break;
                case FieldList.TYPE_STRING:
                    if( value != null ){
                        String str = value.toString();
                        _dout.writeByte( UTF_STRING_MARKER );
                        _dout.writeUTF( str );
                    } else {
                        _dout.writeByte( NULL_STRING_MARKER );
                    }
                    break;
            }
        }
    
        // Writes a set of fields to the given record. The
        // fields must be compatible with the types in
        // the field list.
    
        public synchronized void writeRecord( int recordID,
                                              Object[] fields )
                                     throws IOException,
                                            RecordStoreException {
            writeStream( fields );
            byte[] data = _bout.getByteArray();
            _rs.setRecord( recordID, data, 0, data.length );
        }
    
        // Writes a set of fields to the output stream.
    
        private void writeStream( Object[] fields )
                                           throws IOException {
            int count = _fieldList.getFieldCount();
            int len = ( fields != null ? fields.length : 0 );
    
            prepareForOutput();
    
            for( int i = 0; i < count; ++i ){
                writeField( _fieldList.getFieldType( i ),
                            ( i < len ? fields[i] : null ) );
            }
        }
    
        private DirectByteArrayInputStream  _bin;
        private DirectByteArrayOutputStream _bout;
        private byte[]                      _buffer;
        private DataInputStream             _din;
        private DataOutputStream            _dout;
        private FieldList                   _fieldList;
        private RecordStore                 _rs;
    }
    

    要创建一个 FieldBasedStore,你需要一个 RecordStore 实例和一个 FieldList 实例。您可以在构造 FieldBasedStore 的过程中隐式地从记录库自身中读取后者:

    ...
    RecordStore rs = ... // an open record store
    FieldBasedStore fstore = new FieldBasedStore( rs );
    ...
    

    或者您可以对其进行显式指定:

    ...
    RecordStore rs = ... // an open record store
    FieldList list = ... // a field list
    FieldBasedStore fstore = new FieldBasedStore( rs, list );
    ...
    

    FieldBasedStore 将每条记录作为一个对象数组来处理。数组中的数据类型和字段列表中所描述的字段类型相匹配。在上面的部门列表中,每一个都含有一个部门标识符、一个部门名称以及一个经理标识符。您可以用如下方法添加一条记录:

    ...
    Object[] fields = new Object[]{
        new Short( 1 ), "Accounting", new Integer( 100 )
    };
    
    int recordID = fstore.addRecord( fields );
    ...
    

    注意 FieldBasedStore 中的写记录代码是具有智能性的,它可以执行“明显的”数据转换,所以您也可以用以下方法进行:

    ...
    Object[] fields = new Object[]{ "1", "Accounting", "100" };
    int recordID = fstore.addRecord( fields );
    ...
    

    读取记录也同样简单:

    ...
    Object[] fields = fstore.readRecord( recordID );
    for( int i = 0; i < fields.length; ++i ){
        System.out.println( "Field: " +
           fstore.getFieldList().getFieldName( i ) +
           " Value: " + fields[i] );
    }
    ...
    

    您可以在任何时候重写该数组来修改记录:

    ...
    Object[] fields = fstore.readRecord( recordID );
    fields[2] = "134"; // change the manager
    fstore.writeRecord( recordID, fields );
    ...
    

    这里有一个 MIDlet 的示例,它使用了一对基于字段的记录库来存储和检索雇员和部门数据。它也可使用 第一部分 所述的 RMSAnalyzer 类将记录库中的内容倒出,只是向您表明记录是如何被存储的。

    import java.io.*;
    import java.util.*;
    import javax.microedition.lcdui.*;
    import javax.microedition.midlet.*;
    import javax.microedition.rms.*;
    import j2me.rms.*;
    
    // A simple MIDlet for testing RMS mappings
    // done using the FieldBasedStore class.
    
    public class RMSMappings extends MIDlet
                     implements CommandListener {
    
        private Display    display;
    
        public static final Command exitCommand =
                             new Command( "Exit",
                                          Command.EXIT, 1 ); 
        public static final Command testCommand =
                             new Command( "Test",
                                          Command.SCREEN, 1 );
    
        private static Object[][] empList = {
            new Object[]{ "1", "Mary", "CEO", "100", "F" },
            new Object[]{ "2", "John", "CFO", "200", "M" },
            new Object[]{ "3", "Pat", "Closem", "300", "F" },
            new Object[]{ "4", "PJ", "Admin", "100", "M" },
        };
    
        private static Object[][] deptList = {
            new Object[]{ "100", "Executive", "1" },
            new Object[]{ "200", "Operations", "2" },
            new Object[]{ "300", "Sales", "1" },
        };
    
        public RMSMappings(){
        }
    
        public void commandAction( Command c,
                                   Displayable d ){
            if( c == exitCommand ){
                exitMIDlet();
            } else if( c == testCommand ){
                runTest();
            }
        }
    
        protected void destroyApp( boolean unconditional )
                           throws MIDletStateChangeException {
            exitMIDlet();
        }
    
        public void exitMIDlet(){
            notifyDestroyed();
        }
    
        public Display getDisplay(){ return display; }
    
        protected void initMIDlet(){
            display.setCurrent( new MainForm() );
        }
    
        protected void pauseApp(){
        }
    
        private void printRecord( FieldBasedStore store,
                                  int recordID ){
            try {
                FieldList list = store.getFieldList();
                Object[]  fields = store.readRecord( recordID );
    
                if( fields.length != list.getFieldCount() ){
                    System.out.println( "Error: bad count" );
                    return;
                }
    
                System.out.println( "Record " + recordID + ":" );
    
                for( int i = 0; i < fields.length; ++i ){
                    System.out.println( "  " +
                          list.getFieldName( i ) + ": " +
                          fields[i] );
                }
            }
            catch( RecordStoreException e  ){
            }
            catch( IOException e ){
            }
        }
    
        private void runTest(){
            // First delete the record stores...
    
            System.out.println( "Deleting record stores..." );
    
            String[] names = RecordStore.listRecordStores();
    
            for( int i = 0; i < names.length; ++i ){
                try {
                    RecordStore.deleteRecordStore( names[i] );
                } catch( RecordStoreException e ){
                    System.out.println( "Could not delete " +
                                        names[i] );
                }
            }
    
            // Create two record stores, one with a field list
            // stored in the first record and the second with
            // a field list stored separately (in the app)
    
            RecordStore     empRS = null;
            RecordStore     deptRS = null;
            FieldList       empFields = new FieldList( 5 );
            FieldList       deptFields = new FieldList( 3 );
            FieldBasedStore employees;
            FieldBasedStore departments;
    
            empFields.setFieldType( 0, FieldList.TYPE_INT );
            empFields.setFieldName( 0, "ID" );
            empFields.setFieldType( 1, FieldList.TYPE_STRING );
            empFields.setFieldName( 1, "Given Name" );
            empFields.setFieldType( 2, FieldList.TYPE_STRING );
            empFields.setFieldName( 2, "Last Name" );
            empFields.setFieldType( 3, FieldList.TYPE_BOOLEAN );
            empFields.setFieldName( 3, "Active" );
            empFields.setFieldType( 4, FieldList.TYPE_CHAR );
            empFields.setFieldName( 4, "Sex" );
    
            System.out.println( "Initializing employees" );
    
            try {
                empRS = RecordStore.openRecordStore( "empRS",
                                                     true );
                // now store the field list in the RS
                empFields.toRecordStore( empRS, -1 );
                employees = new FieldBasedStore( empRS );
            }
            catch( RecordStoreException e ){
                System.out.println( "Could not create empRS" );
                return;
            }
            catch( IOException e ){
                System.out.println( "Error storing field list" );
                return;
            }
    
            System.out.println( "Initializing departments" );
    
            deptFields.setFieldType( 0, FieldList.TYPE_INT );
            deptFields.setFieldName( 0, "ID" );
            deptFields.setFieldType( 1, FieldList.TYPE_STRING );
            deptFields.setFieldName( 1, "Name" );
            deptFields.setFieldType( 2, FieldList.TYPE_INT );
            deptFields.setFieldName( 2, "Manager" );
    
            try {
                deptRS = RecordStore.openRecordStore( "deptRS",
                                                      true );
                departments = new FieldBasedStore( deptRS,
                                                   deptFields );
            }
            catch( RecordStoreException e ){
                System.out.println( "Could not create deptRS" );
                return;
            }
    
            int[] empRecordID;
            int[] deptRecordID;
            int   i;
    
            // Add the data...
    
            try {
                empRecordID = new int[ empList.length ];
    
                for( i = 0; i < empList.length; ++i ){
                    empRecordID[i] =
                           employees.addRecord( empList[i] );
                }
    
                deptRecordID = new int[ deptList.length ];
    
                for( i = 0; i < deptList.length; ++i ){
                    deptRecordID[i] =
                        departments.addRecord( deptList[i] );
                }
            }
            catch( RecordStoreException e ){
                System.out.println( "Error adding record" );
                return;
            }
            catch( IOException e ){
                System.out.println( "Error writing field" );
                return;
            }
    
            // Now fetch the data back and print it...
    
            System.out.println( "---- Employee data ----" );
    
            for( i = 0; i < empRecordID.length; ++i ){
                printRecord( employees, empRecordID[i] );
            }
    
            System.out.println( "---- Department data ----" );
        
            for( i = 0; i < deptRecordID.length; ++i ){
                printRecord( departments, deptRecordID[i] );
            }
        
            System.out.println( "Closing empRS" );
    
            try {
                empRS.closeRecordStore();
            }
            catch( RecordStoreException e ){
                System.out.println( "Error closing empRS" );
            }
    
            System.out.println( "Closing deptRS" );
    
            try {
                deptRS.closeRecordStore();
            }
            catch( RecordStoreException e ){
                System.out.println( "Error closing deptRS" );
            }
    
            System.out.println( "Dumping record stores..." );
    
            // Analyze them...
    
            RMSAnalyzer analyzer = new RMSAnalyzer(
                          new RMSAnalyzer.SystemLogger( 10 ) );
            analyzer.analyzeAll();
        }
    
        protected void startApp()
                          throws MIDletStateChangeException {
            if( display == null ){ 
                display = Display.getDisplay( this );
                initMIDlet();
            }
        }
    
        public class MainForm extends Form {
            public MainForm(){
                super( "RMSMappings" );
    
                addCommand( exitCommand );
                addCommand( testCommand );
    
                setCommandListener( RMSMappings.this );
            }
        }
    }
    

    正如它们现在所表明的,FieldBasedStoreFieldList 类可以使用一些改善。例如,基于字段的库也可以基于游标,像一个 JDBC 结果集,并使您在记录之间移动并抽取单个字段值。记录库可以缓冲记录。字段列表可以返回给定名称的字段的索引。当然,这些和其他的改进都是有同样代价的,于是您不得不在需要什么和什么会在您的平台上起作用之间进行平衡。当您预先不知道数据结构时,一个通用的基于字段的方法是最合适的。当然,预先不知道数据结构的情况并不常出现。通常,最好的方案是在一个持久存储对象中封装已知的对象类型。我在这里提出通用的方法,主要是告诉您最多能做些什么。

    下一部分提要

    第四节将讨论使用 RMS 的更高级的方面:遍历记录库和筛选记录。

    关于作者

    Eric Giguere  是来自 Sybase 子公司 iAnywhere Solutions 的一个软件开发人员,主要从事用于手持设备和无线计算的 Java 技术。他拥有 Waterloo 大学的 BMath 和 MMath 学位,并广泛撰写有关计算主题的文章。

    数据库和 MIDP,第二部分:数据映射

     

    数据库和 MIDP,第二部分:数据映射

    作者:Eric Giguere

    正如 第一部分 所述,移动信息设备描述 (Mobile Information Device Profile,MIDP) 通过记录管理系统 (Record Management System,RMS) 提供数据持久存储。MIDP 对持久存储的支持局限于简单的字节数组 (byte array),且记录的读写是按整体进行,而不是按字段进行的。因此,RMS 应用程序编程接口 (API) 非常简单,但它要求应用程序使用一个非常简单的二进制格式进行数据存储。

    本文将描述封装低级存储操作的数据映射策略,目的是提高应用程序存储和检索持久数据的效率。

    数据操作的核心类

    实际上,将数据写到一个记录库 (record store) 和通过网络向服务器发送数据包并没有什么区别。MIDP 基于其上的有限连接设备配置 (Connected Limited Device Configuration,CLDC) 包括源自 J2SE 核心库的标准数据操作类,这些类对于 RMS 操作十分有用。使用通用接口的一个很大的好处是,MIDlet 可以更容易地和运行在标准和企业 Java 平台上的应用程序进行互操作。

    字节数组流

    ByteArrayInputStream 对象可以将字节数组转换成输入流 (input stream),举例如下:

    ...
    byte[] data = new byte[]{ 1, 2, 3 };
    ByteArrayInputStream bin = new ByteArrayInputStream( data );
    
    int b;
    
    while( ( b = bin.read() ) != -1 ){
        System.out.println( b );
    }
    
    try {
        bin.close();
    } catch( IOException e ){
        // never thrown in this case
    }
    ...
    

    输入流将顺序返回该数组中的每个字节,直到数组的尾部。利用 mark()reset(),可以在任何时候重新定位字节数组中的流 (stream)。

    对象 ByteArrayOutputStream 可以捕获内存缓冲区中的数据,以便以后转换成字节数组:

    ...
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    
    bout.write( 1 );
    bout.write( 2 );
    bout.write( 3 );
    
    byte[] data = bout.toByteArray();
    
    for( int i = 0; i < data.length; ++i ){
        System.out.println( data[i] );
    }
    
    try {
        bout.close();
    } catch( IOException e ){
        // never thrown in this case
    }
    ...
    

    随着数据不断地写入流中,ByteArrayOutputStream 的缓冲区的大小会自动增长。toByteArray() 方法将捕获到的数据拷贝到字节数组中。通过调用 reset(),我们就可以重用内部缓冲区,以便进行更多的数据捕获。

    数据流

    DataInputStream 可以将一个原始输入流转换成为原始数据类型和字符串:

    ...
    InputStream in = ... // an input stream
    DataInputStream din = new DataInputStream( in );    
    
    try {
        int custID = din.readInt();
        String lastName = din.readUTF();
        String firstName = din.readUTF();
        long timestamp = din.readLong();
    
        din.close();
    }
    catch( IOException e ){
        // handle the error here
    }
    ...
    

    只有以 DataInputStream 所期望的与机器无关的格式写入 stream 中的数据,数据才可以读取。该类的方法可以读取大多数简单 Java 数据类型,这些方法包括 readBoolean()、readByte()、readChar()、readShort()、readInt() readLong()。CLDC 1.1 实现额外支持 readFloat()readDouble()。也有方法用来读取字节数组和无符号值,这些方法是 readFully()、readUnsignedByte()readUnsignedShort()。 

    readUTF() 方法可读取 UTF-8 格式的字符串,字符串含有的字符数可达 65,535。如果要读取一个两字节字符值的序列的字符串,应用程序必须多次调用 readChar(),它既可将一个分隔符看做字符串的结尾,也可认为字符串的长度已知。长度可以是一个固定的值,也可以写入字符串前部的 stream 中。

    用来读取数据的应用程序必须知道原始数据的写入顺序,以便调用正确的方法。

    DataOutputStream 可将字符串和原始数据类型写入一个输出流 (output stream):

    ...
    OutputStream out = ... // an output stream
    DataOutputStream dout = new DataOutputStream( out );
    
    try {
        dout.writeInt( 100 );
        dout.writeUTF( "Smith" );
        dout.writeUTF( "John" );
        dout.writeLong( System.currentTimeMillis() );
    
        dout.close();
    }
    catch( IOException e ){
        // handle the error here
    }
    ...
    

    这些数据用 DataInputStream 所期望的相同的与机器无关的格式写入。该类的方法可以写入大多数简单的 Java 数据类型,这些方法是:writeBoolean()、writeByte()、writeChar()、writeShort()、writeInt()writeLong()。 CLDC 1.1 实现额外支持 writeFloat()writeDouble()。

    可调用两个方法来写字符串。您可以用 writeUTF() 写一个用 UTF-8 格式编码的最多含有 65,535 个字符的字符串,或者调用 writeChars() 来写一个两字节字符的序列。

    基本数据映射

    标准数据操作类可使基本数据映射变得容易..将数据写入记录库只需要将 DataOutputStreamByteArrayOutputStream 合并,并存储得到的字节数组:

    ...
    RecordStore rs = ... // a record store
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    DataOutputStream dout = new DataOutputStream( bout );
    
    try {
        dout.writeInt( 100 );
        dout.writeUTF( "Smith" );
        dout.writeUTF( "John" );
        dout.writeLong( System.currentTimeMillis() );
        dout.close();
    
        byte[] data = bout.toByteArray();
        rs.addRecord( data, 0, data.length );
    }
    catch( RecordStoreException e ){
        // handle RMS error here
    }
    catch( IOException e ){
        // handle IO error here
    }
    ...
    

    数据的读取是上述的步骤逆过程,它使用一个 DataInputStream 和一个 ByteArrayInputStream:

    ...
    RecordStore rs = ... // a record store
    int recordID = ... // the record to read
    ByteArrayInputStream bin;
    DataInputStream din;
    
    try {
        byte[] data = rs.getRecord( recordID );
         
        bin = new ByteArrayInputStream( data );
        din = new DataInputStream( bin );
        
        int id = din.readInt();
        String lastName = din.readUTF();
        String firstName = din.readUTF();
        long timestamp = din.readLong();
    
        din.close();
    
        ... // process data here
    }
    catch( RecordStoreException e ){
        // handle RMS error here
    }
    catch( IOException e ){
        // handle IO error here
    }
    ...
    

    当写字符串时,需要注意 null 值。在大多数情况下,您会反而写一个空字符串。否则,您将需要用标记字节 (marker byte) 将 null 字符串和空字符串分别开来。

    简单对象映射

    在许多情况下,您要保存的数据被封装到一个对象实例中。例如,Contact 类含有某个人的联系信息:

    package j2me.example;
    
    // The contact information for a person
    
    public class Contact {
        private String _firstName;
        private String _lastName;
        private String _phoneNumber;
    
        public Contact(){
        }
    
        public Contact( String firstName, String lastName,
                        String phoneNumber )
        {
            _firstName = firstName;
            _lastName = lastName;
            _phoneNumber = phoneNumber;
        }
    
        public String getFirstName(){
            return _firstName != null ? _firstName : "";
        }
    
        public String getLastName(){
            return _lastName != null ? _lastName : "";
        }
    
        public String getPhoneNumber(){
            return _phoneNumber != null ? _phoneNumber : "";
        }
    
        public void setFirstName( String name ){
            _firstName = name;
        }
    
        public void setLastName( String name ){
            _lastName = name;
        }
    
        public void setPhoneNumber( String number ){
            _phoneNumber = number;
        }
    }
    

    如果类可以修改,那么引入持久存储的最简单的方法是添加方法,以实现对象和字节数组之间的相互映射:

    public void fromByteArray( byte[] data ) throws IOException {
        ByteArrayInputStream bin = new ByteArrayInputStream(data);
        DataInputStream din = new DataInputStream( bin );
    
        _firstName = din.readUTF();
        _lastName = din.readUTF();
        _phoneNumber = din.readUTF();
    
        din.close();
    }
    
    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream( bout );
    
        dout.writeUTF( getFirstName() );
        dout.writeUTF( getLastName() );
        dout.writeUTF( getPhoneNumber() );
    
        dout.close();
    
        return bout.toByteArray();
    }
    

    那么,存储一个对象就成了一个简单的操作:

    ...
    Contact contact = ... // the contact to store
    RecordStore rs = ... // the record store to use
    
    try {
        byte[] data = contact.toByteArray();
        rs.addRecord( data, 0, data.length );
    }
    catch( RecordStoreException e ){
        // handle the RMS error here
    }
    catch( IOException e ){
        // handle the IO error here
    }
    ...
    

    对象的检索也同样容易:

    ...
    RecordStore rs = ... // the record store to use
    int recordID = ... // the record ID to read from
    Contact contact = new Contact();
    
    try {
        byte[] data = rs.getRecord( recordID );
        contact.fromByteArray( data );
    }
    catch( RecordStoreException e ){
        // handle the RMS error here
    }
    catch( IOException e ){
        // handle the IO error here
    }
    ...
    

    如果类不可修改,如标准 VectorHashtable 类,您就需要写一个帮助器类。例如,这里有一个帮助器类,它可将一组非空字符串映射到一个字节数组:

    package j2me.example;
    
    import java.io.*;
    import java.util.*;
    
    // A helper class that converts transforms string
    // vectors (vectors whose elements are instances of
    // String or StringBuffer ) into byte arrays and vice-versa.
    
    public class StringVectorHelper {
    
        // Reconstitutes a vector from a byte array.
    
        public Vector fromByteArray( byte[] data )
                                     throws IOException {
            ByteArrayInputStream bin =
                     new ByteArrayInputStream( data );
            DataInputStream din = new DataInputStream( bin );
    
            int count = din.readInt();
            Vector v = new Vector( count );
    
            while( count-- > 0 ){
                v.addElement( din.readUTF() );
            }
    
            din.close();
    
            return v;
        }
    
        // Transforms a vector into a byte array.
    
        public byte[] toByteArray( Vector v )
                                   throws IOException {
            ByteArrayOutputStream bout =
                       new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream( bout );
    
            dout.writeInt( v.size() );
    
            Enumeration e = v.elements();
            while( e.hasMoreElements() ){
                Object o = e.nextElement();
                dout.writeUTF( o != null ? o.toString() : "" );
            }
    
            dout.close();
    
            return bout.toByteArray();
        }
    }
    

    当然,您这里所做的一切都是为了开发一个对象串行化框架。一个完整的框架超出了本文的讨论范围,然而当您进行对象持久存储时,以下的问题还是值得仔细研究考虑的:

    • 对象创建。CLDC 并不含有反射 API (reflection API),所以您需要重建持久对象。它可以是一个接收 stream 或者字节数组的构造器,也可以是一个更加复杂的 factory 类。
    • 版本化。如果一个对象的数据布局可以变化,那么您将需要在字节数组的开始处存储一个数字,该数用来标识所存储的对象的版本。
    • 对象引用。如果一个对象引用了另一个对象,那么它们的关系必须保持。在有些情况下,可以用一个索引或者一个键来替代对象引用,索引和键可以在反串行化后对被引用的对象进行定位。否则,您就必须存储一个完整的对象图,而不仅仅是一个单一的对象。

    您可以避免这些问题,或者将它们的影响降低到最低,方法就是只保存和恢复原始 Java 数据类型和简单的、独立的对象。fromByteArray()toByteArray() 方法简单,但它们却是进行对象持久存储的简单有效的方法。您也可以使用这些技术通过网络进行对象的拷贝:一旦对象以字节数组的形式存在,那么,利用网络连接将数组发送到另一个设备或者一个外部服务器,并在另一端重建对象,就成为一个简单的事情。例如,您为 J2ME 应用程序所开发的数据类可以很容易的用于为 J2EE 所开发的 servlet 中。

    使用数据流

    以上的示例直接处理字节数组。尽管它们在保存和恢复一组数据方面很方便,然而,当需要存储多组数据(例如,一组对象)时,对字节数组的处理就成为任务繁重的工作了。一个更好的方法是将字节数组的管理和数据的读写分开。例如,可以向 Contact 类添加如下的方法:

    public void fromDataStream( DataInputStream din ) 
                                throws IOException {
        _firstName = din.readUTF();
        _lastName = din.readUTF();
        _phoneNumber = din.readUTF();
    }
    
    public void toDataStream( DataOutputStream dout ) 
                              throws IOException {
        dout.writeUTF( getFirstName() );
        dout.writeUTF( getLastName() );
        dout.writeUTF( getPhoneNumber() );
    }
    

    为方便起见,我们可以保持现有的 fromByteArraytoByteArray 方法,但是我们应该对它们进行重写以便使用新的面向 stream 的方法:

    public void fromByteArray( byte[] data ) throws IOException {
        ByteArrayInputStream bin = new ByteArrayInputStream(data);
        DataInputStream din = new DataInputStream( bin );
    
        fromDataStream( din );
        din.close();
    }
    
    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream( bout );
    
        toDataStream( dout );
        dout.close();
    
        return bout.toByteArray();
    }
    

    集中数据的读写以确保其一致性。

    下一部分提要

    在本部分中,您已经学习了一些基本数据映射技术。在 第三部分 中,您将了解到一个更成熟的方法来管理持久存储数据,该方法可使您的应用程序存储和检索由多个包含不同数据类型的字段所组成的对象。

    关于作者

    Eric Giguere 是来自 Sybase 子公司 iAnywhere Solutions 的一个软件开发人员,主要从事用于手持设备和无线计算的 Java 技术。他拥有 Waterloo 大学的 BMath 和 MMath 学位,并广泛撰写有关计算主题的文章。

    数据库和 MIDP,第一部分:了解记录管理系统

    记录管理系统(Record Management System,RMS)是移动信息设备描述(Mobile Information Device Profile,MIDP)的一个主要子系统,是一种应用程序编程接口(API),为 MIDP 应用程序提供本地的、基于设备的数据持久存储功能。在各种 MIDP 设备大行其道的今天,RMS 是唯一可以实现本地数据存储的工具——极少设备支持传统的文件系统。可以设想一下,彻底地了解 RMS,对于编写任何依靠持久性本地数据的程序来说,是多么至关重要。

    本文是一系列文章的第一篇。这些文章将探究 RMS,以及解答有关它在 MIDP 应用程序中使用情况的较重要的问题,例如,与外部数据源(如关系数据库)相互作用的问题。开始我们要讨论的是 RMS 提供一些什么内容,并编写几个简单的 RMS 调试诊断程序。

    关键概念

    首先,要理解记录管理系统的一些关键概念。

    记录

    顾名思义,RMS 是用来管理记录的系统。一条记录是一个单一的数据项。RMS 没有对放入记录的内容加以限制:记录可以是数字、字符串、数组和图像——任何一段连续字节可以描述的事物。如果您能将数据编译成二进制代码,并且具备相应的解码手段,那么您就能将其存入一条记录中,当然受系统分配的容量大小的限制。

    许多 RMS 的初学者都对记录这个词感到困惑。“字段在什么地方?”他们奇怪系统是如何将单一记录细分为分散的数据序列。答案很简单:在RMS中,记录不包含任何字段。或者说得更准确些,一条记录只包含一个大小可变的二进制字段。这样,解释记录内容的任务就完全依靠应用程序来完成。RMS 只提供存储和惟一标识符。虽然这样分工导致应用程序趋于复杂化,但是它使得 RMS 小巧灵活——这是作为 MIDP 子系统的一项重要属性。

    在 API 级别中,记录是简单的字节数组。

    记录存储

    记录存储是记录的有序集合。记录不是独立的实体:每一条记录必须从属于一个记录存储,而且所有的记录存取都通过记录存储来实现。事实上,记录存储保证记录读写的自动运行,不会发生数据损坏。

    在创建一条记录时,记录存储为其分配一个惟一的标识符。该标识符是称作记录 ID 的整数。加入记录存储的第一条记录的记录 ID 是 1,第二条的记录 ID 是 2,依此类推。记录 ID不是索引:记录删除后不会对现存的记录进行重新编号,也不会影响后面记录的记录 ID 值。

    在 MIDlet 套件中使用名称来标记记录存储。一个记录存储名可以包括 1 到 32 位 Unicode 字符,而且在创建该记录存储的 MIDlet 套件中该名称必须惟一。在 MIDP1.0 中,记录存储不能被不同的 MIDlet 套件共享。在MIDP2.0 里面,则允许 MIDlet 套件与其他套件共享同一个记录存储,其中此记录存储由 MIDlet 套件的名称,销售商的名称以及记录存储自己的名称共同加以识别。

    记录存储也具有时间戳记和版本信息,所以应用程序能够知道记录存储最后被修改的时间。为了进一步紧密跟踪,应用程序可以注册监听程序以便及时获知记录存储何时被修改。

    在 API level 中,用 javax.microedition.rms.RecordStore 类的实例来代表记录存储。所有的RMS类和接口在 javax.microedition.rms 包中都又定义。

    RMS 外观

    在我们看一些代码之前,先回顾一下几个有关 RMS 的关键信息。

    存储容量限制

    基于记录的数据存储的有效存储空间总量,因设备的不同而不同。MIDP 规范要求设备为持久数据存储,保留至少 8k 非易失性存储空间。该规范没有对个别记录的大小加以限制,可是空间限制会随着设备的不同而发生改变。RMS 提供多种方式决定单条记录的大小,记录存储的总容量,以及保留多少空间用于数据存储。记住持久性存储器是共享的,是珍贵的资源,因此请节约使用。任何使用 RMS 的 MIDlet 套件,都应该通过在 JAR 的清单(manifest)文件和应用程序描述符中设置 MIDlet 数据大小(MIDlet-Data-Size)属性,指定所需的数据存储空间的最小字节数。不要使设置的值大于必须的容量,因为设备有可能拒绝安装那些数据存储要求超过有效空间的 MIDlet 套件。如果该属性缺失,那么设备将会假定此MIDlet 套件不需要数据存储空间。实际上,大多数设备允许应用程序超过它们规定的空间要求,但是请不要依赖这种方式。

    需要指出,某些 MIDP 的实现需要定义其他的一些有关存储需求的属性——请查阅设备文档了解细节。

    速度

    对持久性存储器操作所需的时间,通常都要比对易失性(非持久性)存储器进行相同操作所需的时间更长。特别是写数据,在某些平台上写数据会持续很长时间。为了提高性能,不是从 MIDlet 事件线程中来完成 RMS 操作,而是利用缓存频繁访问易失性存储器中的数据以保证用户接口反应迅速。

    线程安全性

    RMS 操作具有很高的线程安全性,但对任何共享资源而言,各个线程仍必须协调对同一记录存储的数据读写操作。这种协调要求需要采用在不同 MIDlets 中运行的线程,因为在同一个 MIDlet 套件中记录存储是共享的。

    异常

    一般说来,RMS API 中的方法除了会抛出诸如 java.lang.IllegalArgumentException 之类的标准运行时异常之外,还会抛出一个或多个经检查的异常。RMS 异常都包括在 javax.microedition.rms 包内:

    • InvalidRecordIDException 在由于记录 ID 无效而无法执行某个操作时抛出。
    • RecordStoreFullException 在记录存储中没有足够的可用空间时抛出。
    • RecordStoreNotFoundException 在应用程序试图打开一个不存在的记录存储时抛出。
    • RecordStoreNotOpenException 应用程序试图访问已经关闭的记录存储时抛出。
    • RecordStoreException 是其他 4 个异常的总集,并且在出现它们未包括的一般错误时抛出。

    注意,为了精简,这一系列文章中的代码里面的异常处理都被简化或者省略了。

    使用 RMS

    剩下的文章内容讲述的是使用 RMS API 时对记录的一些基本操作。某些操作贯穿于一个工具类 RMSAnalyzer 的开发中,用于记录存储分析。可以将 RMSAnalyzer 用作自己项目的调试诊断程序。

    记录存储探究

    可以通过调用 RecordStore.listRecordStores() 获得 MIDlet 套件中记录存储的列表。这个静态方法返回一个字符串数组,其中每一个字符串代表该 MIDlet 套件中的一个记录存储的名称。如果没有记录存储,则返回为 null。

    方法 RMSAnalyzer.analyzeAll() 使用 listRecordStores() 为套件中的每个记录存储调用 analyze()

    public void analyzeAll(){
        String[] names = RecordStore.listRecordStores();
    
        for( int i = 0;
             names != null && i < names.length;
             ++i ){
            analyze( names[i] );
        }
    }
    

    注意,此数套件标识的只是自身 MIDlet 套件的记录存储。也就是说创建它们的那一个套件。MIDP 规范无论如何不包括列出其他 MIDP 套件记录存储的功能。在 MIDP 1.0 中,自身套件以外的记录存储是根本看不到的。在 MIDP 2.0,自己的套件可以将记录存储指派为可共享的,但其他 MIDlet 套件只有在知道其名称后才能使用它。

    打开和关闭记录存储

    RecordStore.openRecordStore() 用于打开一个记录存储,也可以用于有选择地创建一个记录存储。该静态方法返回一个 RecordStore 对象的实例,如下面的 RMSAnalyzer.analyze() 版本所示:

    public void analyze( String rsName ){
        RecordStore rs = null;
    
        try {
            rs = RecordStore.openRecordStore( rsName, false );
            analyze( rs ); // call overloaded method
        } catch( RecordStoreException e ){
            logger.exception( rsName, e );
        } finally {
            try {
                rs.closeRecordStore();
            } catch( RecordStoreException e ){
                // Ignore this exception
            }
        }
    }
    

    openRecordStore() 的第二个参数表示若该记录存储不存在,是否应该创建它。假如使用的是 MIDP 2.0,并且希望 MIDlet 打开由其他套件建立的记录存储,就可以用以下形式的 openRecordStore()

    ...
    String name = "mySharedRS";
    String vendor = "EricGiguere.com";
    String suite = "TestSuite";
    RecordStore rs = 
          RecordStore.openRecordStore( name, vendor, suite );
    ...
    

    销售商和套件的名称,必须与 MIDlet 套件的清单(manifest)文件和应用程序描述符所定义的名称相匹配。

    完成对记录存储的操作之后,调用 RecordStore.closeRecordStore() 将其关闭,与 analyze() 方法一样。

    RecordStore 实例在一个MIDlet 套件中是惟一的:它一旦被打开,以后用同一名称调用 openRecordStore() 将会返回相同的对象引用。这个实例被该 MIDlet 套件的所有 MIDlet 共享。

    每一个 RecordStore 实例都会跟踪它被打开的次数。除非调用相同次数的 closeRecordStore(),否则记录存储不会被真正关闭。在记录存储关闭之后使用它会抛出 RecordStoreNotOpenException。

    创建记录存储

    调用 openRecordStore() 并将第二个参数设为真来创建一个专用的记录存储:

    ...
    // Create a record store
    RecordStore rs = null;
    
    try {
        rs = RecordStore.openRecordStore( "myrs", true );
    } catch( RecordStoreException e ){
        // couldn't open it or create it
    }
    ...
    

    要执行记录存储的一次性初始化,请在打开记录存储后,立即检查 getNextRecordID() 是否等于 1:

    if( rs.getNextRecordID() == 1 ){
        // perform one-time initialization
    }
    

    或者,要在某个记录存储一旦为空就重新初始化该记录存储,请检查 getNumRecords() 返回的值:

    if( rs.getNumRecords() == 0 ){
        // record store is empty, re-initialize
    }
    

    要创建共享的记录存储(只在 MIDP 2.0 中),请使用 openRecordStore() 的四个参数变量:

    int     authMode = RecordStore.AUTHMODE_ANY;
    boolean writable = true;
    
    rs = RecordStore.openRecordStore( "myrs", true, 
           authMode, writable );
    

    当第二个参数为真且该记录存储已经不存在,则最后两个参数控制它的授权模式以及可写性。授权模式决定其他 MIDlet 套件能否访问该记录存储。有两种可能的模式分别为 RecordStore.AUTHMODE_PRIVATE(只有自身 MIDlet 套件有权访问)和 RecordStore.AUTHMODE_ANY(任何 MIDlet 套件都有权访问)。可写性标记决定其他 MIDlet 套件可否修改此记录存储——如果为假,它们只能从该记录存储中读取数据。

    注意自身 MIDlet 套件能够使用 RecordStore.setMode 随时更改记录存储的权限模式和可写性:

    rs.setMode( RecordStore.AUTHMODE_ANY, false );
    

    实际上,最好用 AUTHMODE_PRIVATE 建立一个共享记录存储,并且在初始化之后将其发布。

    添加与更新记录

    还记得记录是字节数组吧。使用 RecordStore.addRecord() 添加新的记录到打开的记录存储:

    ...
    byte[] data = new byte[]{ 0, 1, 2, 3 };
    int    recordID;
    
    recordID = rs.addRecord( data, 0, data.length );
    ...
    

    通过将第一个参数设为 null 来添加一个空记录。第二个和第三个参数指定了数组的开始偏移量及从该偏移量开始要存储的总字节数。添加成功后返回新纪录的 ID,否则抛出类似于 RecordStoreFullException 的异常。

    可以用 RecordStore.setRecord() 随时更新记录:

    ...
    int    recordID = ...; // some record ID
    byte[] data = new byte[]{ 0, 10, 20, 30 };
    
    rs.setRecord( recordID, data, 1, 2 ); 
        // replaces all data in record with 10, 20
    ...
    

    不能批次添加或者更新记录:您必须在内存中作为字节数组的形式构建整个记录,而且使用一次调用来添加或更新记录。

    您能通过调用 RecordStore.getNextRecordID(),找出下一个记录标识符调用 addRecordStore() 时会返回什么值。所有当前的记录标识符都会小于这个值。

    在第二篇文章中我们将着眼于将对象和其他数据转化为字节数组的策略。

    读取记录

    可以利用 RecordStore.getRecord() 的两种形式中的一种读取记录。第一种形式分配合适大小的字节数组并将记录数据复制到里面:

    ...
    int    recordID = .... // some record ID
    byte[] data = rs.getRecord( recordID );
    ...
    

    第二种形式是从指定的偏移量开始,将数据复制到预分配的数组中并返回所复制的字节数量:

    ...
    int    recordID = ...; // some record ID
    byte[] data = ...; // an array
    int    offset = ...; // the starting offset
    
    int numCopied = rs.getRecord( recordID, data, offset );
    ...
    

    数组必须有足够的容量来容纳数据,否则会抛出 java.lang.ArrayIndexOutOfBoundsException。使用 RecordStore.getRecordSize() 返回的值指定一个足够大的数组。实际上,etRecord() 的第一种形式相当于:

    ...
    byte[] data = new byte[ rs.getRecordSize( recordID ) ];
    rs.getRecord( recordID, data, 0 );
    ...
    

    第二种形式在对于一系列记录的迭代操作时,进行最小化存储器分配工作很有帮助。例如,可以使用它和 getNextRecordID()及 getRecordSize() 一起来执行在记录存储中对所有记录的强制搜索:

    ...
    int    nextID = rs.getNextRecordID();
    byte[] data = null;
    
    for( int id = 0; id < nextID; ++id ){
        try {
            int size = rs.getRecordSize( id );
    
            if( data == null || data.length < size ){
                data = new byte[ size ];
            }
    
            rs.getRecord( id, data, 0 );
    
            processRecord( rs, id, data, size ); // process it
        } catch( InvalidRecordIDException e ){
            // ignore, move to next record
        } catch( RecordStoreException e ){
            handleError( rs, id, e ); // call an error routine
        }
    }
    ...
    

    但是,更好的方法是使用 RecordStore.enumerateRecords() 对记录进行迭代操作。我的第三篇文章将讨论 enumerateRecords() 的使用。

    删除记录和记录存储

    RecordStore.deleteRecord() 删除记录:

    ...
    int recordID = ...; // some record ID
    rs.deleteRecord( recordID );
    ...
    

    一旦记录被删除,任何使用它的企图都会抛出 InvalidRecordIDException

    您用 RecordStore.deleteRecordStore() 删除记录存储:

    ...
    try {
        RecordStore.deleteRecordStore( "myrs" );
    } catch( RecordStoreNotFoundException e ){
        // no such record store
    } catch( RecordStoreException e ){
        // somebody has it open
    }
    ...
    

    记录存储只有当其未打开时才能被删除,并且只能由自身 MIDlet 套件中的 MIDlet 来执行。

    其他操作

    剩下还有几个 RMS 操作,它们都是属于 RecordStore 类的方法:

    • getLastModified() 返回上次修改记录存储的时间,与 System.currentTimeMillis() 返回的格式相同。
    • getName() 返回记录存储的名称。
    • getNumRecords() 返回记录存储中的记录数。
    • getSize() 以字节为单位返回记录存储的总大小。总量包括所有记录的总大小以及系统执行记录存储所需的额外开销。
    • getSizeAvailable() 返回记录存储能增加的可用字节数。注意实际的可用大小可能比存储个别记录花费的额外开销要小。
    • getVersion() 返回记录存储的版本号。版本号是大于零的正整数,每次记录存储更改后都会增加。

    MIDlet 也能使用 addRecordListener() 注册监听程序来跟踪记录存储的改变,然后可以用 removeRecordListener() 撤销注册。我将会在第三篇文章中讨论这些监听程序。

    RMSAnalyzer

    第一篇以 RMSAnalyzer 类——记录存储的分析器——的源代码作为结束。下面是分析记录存储的代码:

    ...
    RecordStore rs = ...; // open the record store
    RMSAnalyzer analyzer = new RMSAnalyzer();
    analyzer.analyze( rs );
    ...
    

    默认情况下,分析程序转至 System.out stream 流,请看下面的代码:

    =========================================
    Record store: recordstore2
        Number of records = 4
        Total size = 304
        Version = 4
        Last modified = 1070745507485
        Size available = 975950
    
    Record #1 of length 56 bytes
    5f 62 06 75 2e 6b 1c 42 58 3f  _b.u.k.BX?
    1e 2e 6a 24 74 29 7c 56 30 32  ..j$t)|V02
    5f 67 5a 13 47 7a 77 68 7d 49  _gZ.Gzwh}I
    50 74 50 20 6b 14 78 60 58 4b  PtP k.x`XK
    1a 61 67 20 53 65 0a 2f 23 2b  .ag Se./#+
    16 42 10 4e 37 6f              .B.N7o    
    Record #2 of length 35 bytes
    22 4b 19 22 15 7d 74 1f 65 26  "K.".}t.e&
    4e 1e 50 62 50 6e 4f 47 6a 26  N.PbPnOGj&
    31 11 74 36 7a 0a 33 51 61 0e  1.t6z.3Qa.
    04 75 6a 2a 2a                 .uj**     
    Record #3 of length 5 bytes
    47 04 43 22 1f                 G.C".     
    Record #4 of length 57 bytes
    6b 6f 42 1d 5b 65 2f 72 0f 7a  koB.[e/r.z
    2a 6e 07 57 51 71 5f 68 4c 5c  *n.WQq_hL\
    1a 2a 44 7b 02 7d 19 73 4f 0b  .*D{.}.sO.
    75 03 34 58 17 19 5e 6a 5e 80  u.4X..^j^?
    2a 39 28 5c 4a 4e 21 57 4d 75  *9(\JN!WMu
    80 68 06 26 3b 77 33           ?h.&;w3   
    
    Actual size of records = 153
    -----------------------------------------
    

    这种格式在使用 J2ME 无线工具包测试时很方便。为了在实际设备上测试,可以将分析结果发送到串口或者直接通过网络传输至 servlet。您可以通过定义一个自己的类来达到这个目的。这个类实现 RMSAnalyzer.Logger 接口并将该类的一个实例传输到 RMSAnalyzer 构造程序。

    附在本文之后的是一个称为 RMSAnalyzerTest 用于示范分析程序使用的 J2ME 无线工具包项目:

    package com.ericgiguere;
    
    import java.io.*;
    import javax.microedition.rms.*;
    
    // Analyzes the contents of a record store.
    // By default prints the analysis to System.out,
    // but you can change this by implementing your
    // own Logger.
    
    public class RMSAnalyzer {
    
        // The logging interface.
    
        public interface Logger {
            void logEnd( RecordStore rs );
            void logException( String name, Throwable e );
            void logException( RecordStore rs, Throwable e );
            void logRecord( RecordStore rs, int id,
                            byte[] data, int size );
            void logStart( RecordStore rs );
        }
    
        private Logger logger;
    
        // Constructs an analyzer that logs to System.out.
    
        public RMSAnalyzer(){
            this( null );
        }
    
        // Constructs an analyzer that logs to the given logger.
    
        public RMSAnalyzer( Logger logger ){
            this.logger = ( logger != null ) ? logger :
                                               new SystemLogger();
        }
    
        // Open the record stores owned by this MIDlet suite
        // and analyze their contents.
    
        public void analyzeAll(){
            String[] names = RecordStore.listRecordStores();
    
            for( int i = 0;
                 names != null && i < names.length;
                 ++i ){
                analyze( names[i] );
            }
        }
    
        // Open a record store by name and analyze its contents.
    
        public void analyze( String rsName ){
            RecordStore rs = null;
    
            try {
                rs = RecordStore.openRecordStore( rsName, false );
                analyze( rs );
            } catch( RecordStoreException e ){
                logger.logException( rsName, e );
            } finally {
                try {
                    rs.closeRecordStore();
                } catch( RecordStoreException e ){
                    // Ignore this exception
                }
            }
        }
    
        // Analyze the contents of an open record store using
        // a simple brute force search through the record store.
    
        public synchronized void analyze( RecordStore rs ){
            try {
                logger.logStart( rs );
    
                int    lastID = rs.getNextRecordID();
                int    numRecords = rs.getNumRecords();
                int    count = 0;
                byte[] data = null;
    
                for( int id = 0;
                     id < lastID && count < numRecords;
                     ++id ){
                    try {
                        int size = rs.getRecordSize( id );
    
                        // Make sure data array is big enough,
                        // plus add some for growth
    
                        if( data == null || data.length < size ){
                            data = new byte[ size + 20 ];
                        }
    
                        rs.getRecord( id, data, 0 );
                        logger.logRecord( rs, id, data, size );
    
                        ++count; // only increase if record exists
                    }
                    catch( InvalidRecordIDException e ){
                        // just ignore and move to the next one
                    }
                    catch( RecordStoreException e ){
                        logger.logException( rs, e );
                    }
                }
    
            } catch( RecordStoreException e ){
                logger.logException( rs, e );
            } finally {
                logger.logEnd( rs );
            }
        }
    
        // A logger that outputs to a PrintStream.
    
        public static class PrintStreamLogger implements Logger {
            public static final int COLS_MIN = 10;
            public static final int COLS_DEFAULT = 20;
    
            private int          cols;
            private int          numBytes;
            private StringBuffer hBuf;
            private StringBuffer cBuf;
            private StringBuffer pBuf;
            private PrintStream  out;
    
            public PrintStreamLogger( PrintStream out ){
                this( out, COLS_DEFAULT );
            }
    
            public PrintStreamLogger( PrintStream out, int cols ){
                this.out = out;
                this.cols = ( cols > COLS_MIN ? cols : COLS_MIN );
            }
    
            private char convertChar( char ch ){
                if( ch < 0x20 ) return '.';
                return ch;
            }
    
            public void logEnd( RecordStore rs ){
                out.println( "\nActual size of records = "
                             + numBytes );
                printChar( '-', cols * 4 + 1 );
    
                hBuf = null;
                cBuf = null;
                pBuf = null;
            }
    
            public void logException( String name, Throwable e ){
                out.println( "Exception while analyzing " +
                             name + ": " + e );
            }
    
            public void logException( RecordStore rs, Throwable e ){
                String name;
    
                try {
                    name = rs.getName();
                } catch( RecordStoreException rse ){
                    name = "";
                }
    
                logException( name, e );
            }
    
            public void logRecord( RecordStore rs, int id,
                                   byte[] data, int len ){
                if( len < 0 && data != null ){
                    len = data.length;
                }
    
                hBuf.setLength( 0 );
                cBuf.setLength( 0 );
    
                numBytes += len;
    
                out.println( "Record #" + id + " of length "
                             + len + " bytes" );
    
                for( int i = 0; i < len; ++i ){
                    int    b = Math.abs( data[i] );
                    String hStr = Integer.toHexString( b );
    
                    if( b < 0x10 ){
                        hBuf.append( '0');
                    }
    
                    hBuf.append( hStr );
                    hBuf.append( ' ' );
    
                    cBuf.append( convertChar( (char) b ) );
    
                    if( cBuf.length() == cols ){
                        out.println( hBuf + " " + cBuf );
    
                        hBuf.setLength( 0 );
                        cBuf.setLength( 0 );
                    }
                }
    
                len = cBuf.length();
    
                if( len > 0 ){
                    while( len++ < cols ){
                        hBuf.append( "   " );
                        cBuf.append( ' ' );
                    }
    
                    out.println( hBuf + " " + cBuf );
                }
            }
    
            public void logStart( RecordStore rs ){
                hBuf = new StringBuffer( cols * 3 );
                cBuf = new StringBuffer( cols );
                pBuf = new StringBuffer();
    
                printChar( '=', cols * 4 + 1 );
    
                numBytes = 0;
    
                try {
                    out.println( "Record store: "
                                 + rs.getName() );
                    out.println( "    Number of records = "
                                 + rs.getNumRecords() );
                    out.println( "    Total size = "
                                 + rs.getSize() );
                    out.println( "    Version = "
                                 + rs.getVersion() );
                    out.println( "    Last modified = "
                                 + rs.getLastModified() );
                    out.println( "    Size available = "
                                 + rs.getSizeAvailable() );
                    out.println( "" );
                } catch( RecordStoreException e ){
                    logException( rs, e );
                }
            }
    
            private void printChar( char ch, int num ){
                pBuf.setLength( 0 );
                while( num-- > 0 ){
                    pBuf.append( ch );
                }
                out.println( pBuf.toString() );
            }
        }
    
        // A logger that outputs to System.out.
    
        public static class SystemLogger
                            extends PrintStreamLogger {
            public SystemLogger(){
                super( System.out );
            }
    
            public SystemLogger( int cols ){
                super( System.out, cols );
            }
        }
    

    下一篇是什么

    在第二篇中,我们将讨论数据映射。

    February 01

    2D游戏开发必备算法之_卡马克地图卷轴算法

    卡马克算法的思想:卡马克算法是在进行2D游戏地图卷动的算法中内存痕迹最小、效率适中的算法之一.其核心的思想就是把地图卷动过程中移出屏幕(不需要在显示的部分)所占用的buffer区域,绘制上新的需要图块,在往真实屏幕上绘制的时候,通过四次绘制buffer把完整的地图重现.
    使用的方法:我们在实际的代码编写中按以下的方式进行.根据上面的基本思想,把地图分为四个块(十字形的将buffer划分为四块),用KaMaKeX和KaMaKeY来记录十字分区的中心坐标(相对于buffer的坐标,我把这个点叫卡马克分区点).当地图向右移动的时候这时把卡马克分区点的坐标X方向加上一个tile的width,然后在从现在的卡马克分区点的坐标Y开始绘制提取出来的tileID对应的图象,注意是从当前的卡马克分区点的坐标Y开始绘制,当超出buffer Height时在从0开始绘制直到结束,这样就完成了在水平方向上的更新.还有就是在水平移动卡马克分区点的时候是在buffer中循环的,也就是从0到buffer Width的一个循环过程,Y方向上完全一致.
    最后是绘制过程,也就是将四个分区绘制出来,口诀就是左变友上变下,呵呵!掌握好卡马克算法对手游开发很有帮助的.

    数独游戏算法

     


    public class ShuDu {
          /**存储数字的数组*/
          static int[][] n = new int[9][9];
          /**生成随机数字的源数组,随机数字从该数组中产生*/
          static int[] num = {1,2,3,4,5,6,7,8,9};
          public static void main(String[] args) {
                 //生成数字
                for(int i = 0;i < 9;i++){
                         //尝试填充的数字次数
                         int time = 0;
                        //填充数字
                         for(int j = 0;j < 9;j++){
                                //产生数字
                                n[ i ][j] = generateNum(time);   
                               //如果返回值为0,则代表卡住,退回处理
                               //退回处理的原则是:如果不是第一列,则先倒退到前一列,否则倒退到前一行的最后一列
                               if(n[j] == 0){
                                       //不是第一列,则倒退一列
                                       if(j > 0){
                                                   j-=2;
                                                   continue;
                                      }else{//是第一列,则倒退到上一行的最后一列
                                                  i--;
                                                  j = 8;
                                                  continue;
                                      }
                                }
                               //填充成功
                                if(isCorret(i,j)){
                                        //初始化time,为下一次填充做准备
                                      time = 0;
                                }else{ //继续填充
                                     //次数增加1
                                     time++;
                                    //继续填充当前格
                                     j--;
                               }   
                        }
                 }
                //输出结果
                for(int i = 0;i < 9;i++){
                           for(int j = 0;j < 9;j++){
                                      System.out.print(n[j] + "  ");
                           }
                           System.out.println();
                 }
          }
      
          /**
            * 是否满足行、列和3X3区域不重复的要求
            * @param row 行号
            * @param col 列号
            * @return true代表符合要求
           */
           public static boolean isCorret(int row,int col){
                  return (checkRow(row) & checkLine(col) & checkNine(row,col));
           }
           /*J2meMobile社区*/
           /**
            * 检查行是否符合要求
            * @param row 检查的行号
            * @return true代表符合要求
            */
           public static boolean checkRow(int row){  
                   for(int j = 0;j < 8;j++){
                          if(n[row][j] == 0){
                                   continue;
                          }
                         for(int k =j + 1;k< 9;k++){
                                  if(n[row][j] == n[row][k]){
                                           return false;
                                  }
                         }
                    }
                   return true;
           }
      
          /**
           * 检查列是否符合要求
           * @param col 检查的列号
           * @return true代表符合要求
           */
          public static boolean checkLine(int col){
                 for(int j = 0;j < 8;j++){
                        if(n[j][col] == 0){
                                continue;
                        }
                        for(int k =j + 1;k< 9;k++){
                                if(n[j][col] == n[k][col]){
                                         return false;
                                }
                        }
                }
               return true;
           }
      
           /**
           * 检查3X3区域是否符合要求
           * @param row 检查的行号
           * @param col 检查的列号
           * @return true代表符合要求
           */
           public static boolean checkNine(int row,int col){
                   //获得左上角的坐标
                   int j = row / 3 * 3;
                   int k = col /3 * 3;
                   //循环比较
                   for(int i = 0;i < 8;i++){
                             if(n[j + i/3][k + i % 3] == 0){
                                         continue;
                             }
                            for(int m = i+ 1;m < 9;m++){
                                        if(n[j + i/3][k + i % 3] == n[j + m/3][k + m % 3]){
                                                     return false;
                                        }
                             }
                     }
                     return true;
            }
      
          /**
           * 产生1-9之间的随机数字
           * 规则:生成的随机数字放置在数组8-time下标的位置,随着time的增加,已经尝试过的数字将不会在取到
           * 说明:即第一次次是从所有数字中随机,第二次时从前八个数字中随机,依次类推,
           *       这样既保证随机,也不会再重复取已经不符合要求的数字,提高程序的效率
           * 这个规则是本算法的核心
           * @param time 填充的次数,0代表第一次填充
           * @return
           */
           public static int generateNum(int time){
                   //第一次尝试时,初始化随机数字源数组
                   if(time == 0){
                         for(int i = 0;i < 9;i++){
                                 num = i + 1;
                         }
                   }
                   //第10次填充,表明该位置已经卡住,则返回0,由主程序处理退回
                   if(time == 9){
                            return 0;
                   }  
                   //不是第一次填充
                   //生成随机数字,该数字是数组的下标,取数组num中该下标对应的数字为随机数字
                  int ranNum = (int)(Math.random() * (9 - time));
                  //把数字放置在数组倒数第time个位置,
                  int temp = num[8 - time];
                  num[8 - time] = num[ranNum];
                  num[ranNum] = temp;
                 //返回数字
                 return num[8 - time];  
           }
    }