Wednesday, April 30, 2008

数据库和 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 );
}
}

数据库和 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,第三部分:使用数据映射


本系列中的 第二部分 介绍了数据映射的基本知识。您学会了如何在字节数组(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,第 4 部分:过滤和遍历策略


作者:Eric Giguere
2004 年 6 月

在本系列文章的第 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 大学计算机科学的学士和硕士学位,并广泛撰写有关计算主题的文章。