WELCOME to the Java Developer ConnectionSM (JDC) JavaTM 2 Platform, Micro Edition (J2METM) Tech Tips, for March 19, 2001. This issue covers:
The J2ME Tech Tips are written by Eric Giguere (http://www.ericgiguere.com), an engineer at iAnywhere Solutions, inc, and author of the book "Java 2 Micro Edition: Professional Developer's Guide."
USING RECORD STORES EFFICIENTLY
The February 20, 2001 J2ME Tech Tip "Record Management System Basics" introduced the Record Management System (RMS), a set of classes for persistently storing and retrieving data. RMS is defined in the Mobile Information Device Profile (MIDP). If you read the tip, you discovered how simple it is to create and use record stores. This tip focuses on getting data into and out of a record store as quickly and efficiently as possible.
The data in a record store is stored in chunks called records. A record is a set of arbitrary binary data, basically an array of bytes. You always read or write an entire record at once, using the getRecord , addRecord and setRecord methods of the RecordStore class. Here's the general pattern:
RecordStore rs = null;
byte[] data;
int id = ....; // assign a record ID
try {
rs = RecordStore.openRecordStore( "mydata", false );
data = rs.getRecord( id );
..... // modify the data
rs.setRecord( id, data, 0, data.length );
rs.closeRecordStore();
}
catch( Exception e ){
// do something here
}
There's no way to change just part of a record: you must read in the entire record, make the change to the data in memory, and then write the entire record back to the record store.
This brings up a good rule regarding using data stores efficiently: minimize the number of times you read or write data to a record store. At the very least, minimize the number of writes; on some devices a write operation is much more expensive than a read operation. Palm devices, for example, write-protect most of their memory to prevent applications from corrupting each other's data. In such an environment, the fewer writes you do, the faster the application runs.
Even if you limit the number of times you write data back to a record store, you might find that reading the data becomes a bottleneck in your application. This is especially true when enumerating the records. Part of the problem often lies in the excessive object "churning" that occurs. Churning refers to the act of continually creating and discarding objects (including arrays) from the memory heap. Not only is object creation expensive as far as time, but churning can also overwhelm the garbage collector.
Here, for example, is one way of cycling through all the records in a record store:
RecordStore rs = ....; // an open record store
try {
int lastID = rs.getNextRecordID();
byte[] data;
for( int i = 0; i < lastID; ++i ){
try {
data = rs.getRecord( i );
.... // do something with the data
}
catch( InvalidRecordIDException e ){
continue;
}
}
}
catch( Exception e ){
// error
}
Another way is to use an enumeration:
RecordStore rs = ....; // an open record store
try {
RecordEnumeration enum = rs.enumerateRecords(
null, null, false );
while( enum.hasNextElement() ){
byte[] data = enum.nextRecord();
.... // do something with the data
}
}
catch( Exception e ){
// error
}
The problem with both approaches is that they allocate new array objects for each record, a classic case of object churning. A better approach is to reuse the same array, discarding it only when it's too small. For example:
RecordStore rs = ....; // an open record store
try {
RecordEnumeration enum = rs.enumerateRecords(
null, null, false );
byte[] data = new byte[100];
int len = 0;
while( enum.hasNextElement() ){
int id = enum.nextRecordId();
len = rs.getRecordSize( id );
if( len > data.length ){
// add a growth factor
data = new byte[ len + 40 ];
}
rs.getRecord( id, data, 0 );
// do something with the data
}
}
catch( Exception e ){
// error
}
Notice how the array automatically grows to accommodate the size of a record. Of course, if each record is the same (fixed) size, then you don't even have to worry about growing the array.
The second rule is when reading records, reuse byte arrays if possible. The same rule about reusing byte arrays also holds for writing records. However, object churning isn't always a problem when writing data because records tend to be read more than they're written.
Reusing the same array isn't usually enough to avoid object churning. This is because the recommended way to read the data is to use a DataInputStream object, as in the following example:
byte[] data = .....; // data from a record store
ByteArrayInputStream bin = new ByteArrayInputStream( data );
DataInputStream din = new DataInputStream( bin );
String name = din.readUTF();
boolean isFemale = din.readBoolean();
// etc. etc.
If you create a new set of input streams for each record you read, then you're still creating and discarding too many objects. However, if you reuse the raw byte array, you can also reuse the input streams. You can do this by using the reset method to make the streams read from the beginning of the array. For example, consider reading a set of records, where each record consists of a boolean value and two integer values:
RecordStoreEnumeration enum = ...; // get a record enumeration
byte[] data = new byte[9]; // record size
ByteArrayInputStream bin = new ByteArrayInputStream( data );
DataInputStream din = new DataInputStream( bin );
while( enum.hasNextElement() ){
int id = enum.nextRecordId();
getRecord( id, data, 0 );
din.reset(); // move stream back to start
boolean first = din.readBoolean();
int second = din.readInt();
int third = din.readInt();
// do something here
}
This is a more efficient way to read records. So the third rule of efficient record usage is: reuse the input streams whenever you reuse the raw byte array.
Let's wrap up the discussion of efficient record usage by writing a simple Record class that you can use to read records:
import java.io.*;
import javax.microedition.rms.*;
public class Record implements DataInput {
private RecordStore _rs;
private byte[] _data;
private int _length;
private int _id;
private DataInputStream _din;
public Record( RecordStore rs ){
this( rs, 100 );
}
public Record(
RecordStore rs, int initialRecordSize ){
_rs = rs;
_data = new byte[ initialRecordSize ];
_din = new DataInputStream(
new ByteArrayInputStream( _data ) );
_length = -1;
}
public byte[] getByteArray() { return _data; }
public int getLength() { return _length; }
public byte[] moveTo( int id )
throws RecordStoreNotOpenException,
InvalidRecordIDException,
RecordStoreException,
IOException
{
_length = _rs.getRecordSize( id );
if( _length > _data.length ){
_data = new byte[ _length + 40 ];
_din = new DataInputStream(
new ByteArrayInputStream( _data ) );
}
_rs.getRecord( id, _data, 0 );
_id = id;
_din.reset();
return _data;
}
public void readFully(byte b[])
throws IOException {
_din.readFully( b );
}
public void readFully(byte b[], int off, int len)
throws IOException {
_din.readFully( b, off, len );
}
return _din.skipBytes( n );
}
public boolean readBoolean() throws IOException {
return _din.readBoolean();
}
public byte readByte() throws IOException {
return _din.readByte();
}
public int readUnsignedByte()
throws IOException {
return _din.readUnsignedByte();
}
public short readShort() throws IOException {
return _din.readShort();
}
public int readUnsignedShort()
throws IOException {
return _din.readUnsignedShort();
}
public char readChar() throws IOException {
return _din.readChar();
}
public int readInt() throws IOException {
return _din.readInt();
}
public long readLong() throws IOException {
return _din.readLong();
}
public String readUTF() throws IOException {
return _din.readUTF();
}
}
You then use the class like this:
try {
rs = RecordStore.openRecordStore( "mydata", true );
// Write two records to the record store
ByteArrayOutputStream bout =
new ByteArrayOutputStream();
DataOutputStream dout =
new DataOutputStream( bout );
byte[] data;
dout.writeUTF( "this is a test" );
dout.writeInt( 1 );
dout.flush();
data = bout.toByteArray();
rs.addRecord( data, 0, data.length );
bout.reset();
dout.writeUTF( "this is another test" );
dout.writeInt( 99 );
dout.flush();
data = bout.toByteArray();
rs.addRecord( data, 0, data.length );
// Now read through the record store
Record record = new Record( rs );
int lastID = rs.getNextRecordID();
RecordEnumeration enum = rs.enumerateRecords(
null, null,
while( enum.hasNextElement() ){
int id = enum.nextRecordId();
record.moveTo( id );
System.out.println( record.readUTF() + " " +
record.readInt() );
}
rs.closeRecordStore();
}
catch( Exception e ){
// handle error
}
You could easily extend the Record class to include methods such as getFirstName and getLastName to make it easy to extract the data you've stored in a record.
USING THE MIDP LOW-LEVEL USER INTERFACE API
With J2ME, profiles are responsible for defining the user interface (UI) application programming interface (API). The Mobile Information Device Profile (MIDP) defines two such APIs, referred to as the high-level and low-level APIs. The high-level API requires you to use task-oriented abstractions to define what the user interface does. You have no real control over what gets drawn on the screen -- the implementation selects the best approach for the device. The high-level API is portable across all MIDP-enabled devices and is really geared towards business applications. Look for more information about the high-level API in a future J2ME Tech Tip.
The low-level API is aimed squarely at game developers. Unlike the high-level API, the low-level API gives you complete access to the screen and to input events. However, this access comes with a price, because you're then responsible for drawing everything that is shown on the screen. You can use both the high-level and low-level APIs in the same application, but not at the same time. Think of the application as a deck of cards, with only one card visible at a time (much like the behavior provided by the java.awt.CardLayout class in the J2SETM Platform). Each card, known as a screen in MIDP terminology, in the deck can use either the high-level API or the low-level API, but not both. The only exception applies to the use of command objects, discussed later in this tip.
To use the low-level API in a MIDlet, you must write a class that extends the Canvas class:
// Simple canvas
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public class MyCanvas extends Canvas {
private MIDlet midlet;
public MyCanvas( MIDlet midlet ){
this.midlet = midlet;
}
protected void paint( Graphics g ){
g.setColor( 255, 255, 255 );
g.fillRect( 0, 0, getWidth(), getHeight() );
g.setColor( 0, 0, 0 );
g.drawString( "Hello there!", getWidth()/2, 0,
g.TOP | g.HCENTER );
}
}
All user interface classes are in the javax.microedition.lcdui package. Notice that you also need to import the javax.microedition.midlet package because you will pass to each canvas a reference to the MIDlet. Your canvas subclass must implement a paint method, which is called by the system to repaint the screen. The paint method is passed a Graphics object which defines methods for all the standard drawing primitives you would expect, such as drawArc , drawLine , drawRect , and drawString . Color is supported in the API using a 24-bit RGB model, although the device might support less color than the API. The MyCanvas example simply erases the screen by painting it white and then draws a string (in black) at the center top of the screen.
You activate a canvas by calling the setCurrent method on the MIDlet's Display object, usually in the startApp method of the application's MIDlet class:
// Simple MIDlet
import javax.microedition.midlet.*;
public class MyMIDlet extends MIDlet {
private Display display;
private MyCanvas canvas;
public MyMIDlet(){
display = Display.getDisplay( this );
canvas = new MyCanvas( this );
}
protected void startApp(){
display.setCurrent( canvas );
}
protected void pauseApp(){
}
protected void destroyApp( boolean unconditional ){
}
public void exit(){
destroyApp( true );
notifyDestroyed();
}
}
Although this MIDlet works, it has one problem: there is no obvious way to exit from it. You need to trap user input in some way. There are two ways to do this: using raw input events or command events.
A canvas receives raw input events by overriding the appropriate event delivery method defined by the Canvas class. There are methods for events generated by:
- Pressing the keypad (
keyPressed , keyRepeated , and keyReleased )
- Using the pointer (
pointerPressed , pointerDragged and pointerReleased ) if a pointer is available on the device
- Showing the canvas (
showNotify , hideNotify ).
For example, you could add a way to terminate the application by defining a keyPressed event on the canvas:
protected void keyPressed( int keyCode ){
((MyMIDlet) midlet).exit();
}
In all keypad events, the keyCode identifies the key that triggered the event. A positive value represents a Unicode character, while a negative value is a key that cannot obviously be converted into Unicode. Rather than figure out on a device-by-device basis what keys map to what, the Canvas class defines a number of constants for commonly-used keys. In particular, it defines abstract game actions (UP, DOWN, LEFT, RIGHT, FIRE, GAME_A, GAME_B, GAME_C, and GAME_D) whose keycode mappings can be determined at runtime. During its initialization, the device can call Canvas.getGameAction to choose which keys map best to which actions.
You might define a base class like this:
public abstract class GameCanvas extends Canvas {
protected MIDlet midlet;
protected int fireKey;
protected int leftKey;
protected int rightKey;
protected int upKey;
protected int downKey;
public GameCanvas( MIDlet midlet ){
this.midlet = midlet;
fireKey = getKeyCode( FIRE );
leftKey = getKeyCode( LEFT );
rightKey = getKeyCode( RIGHT );
upKey = getKeyCode( UP );
downKey = getKeyCode( DOWN );
}
}
And then extend it like this:
public class MyCanvas extends GameCanvas {
private String message = "Press any key";
public MyCanvas( MIDlet midlet ){
super( midlet );
}
protected void paint( Graphics g ){
g.setColor( 255, 255, 255 );
g.fillRect( 0, 0, getWidth(), getHeight() );
g.setColor( 0, 0, 0 );
g.drawString( message, getWidth()/2, 0,
g.TOP | g.HCENTER );
}
protected void keyPressed( int keyCode ){
if( keyCode == fireKey ){
message = "FIRE";
} else if( keyCode == leftKey ){
message = "LEFT";
} else if( keyCode == rightKey ){
message = "RIGHT";
} else if( keyCode == upKey ){
message = "UP";
} else if( keyCode == downKey ){
message = "DOWN";
} else {
message = getKeyName( keyCode );
}
repaint();
}
}
Pointer events are optional because not all MIDP-enabled devices have a pointer. You should take advantage of a pointer whenever it makes sense. But you shouldn't assume that one is available. You can check if pointer events will be triggered, by calling Canvas.hasPointerEvents . The pointer event methods get passed the horizontal and vertical position of the pointer:
protected void pointerPressed( int x, int y ){
// do something here
}
The other way to trap user input is to attach commands to a canvas. A command is an abstract representation of an action. It has a user-defined label, a type, and a priority. The device uses the type to map the command to an appropriate key or button. For example, if the device has a standard OK button, specifying a command type of OK ensures that the OK button will trigger the command. The available types are BACK, CANCEL, EXIT, HELP, ITEM, OK, SCREEN, and STOP. Some or all of these may map onto the same key or button. So when there are conflicts, the priority can be used by the device to order the commands appropriately. The priority is a positive integer, with 1 being the highest priority.
Commands are created using the Command class, as in the following:
Command exitCommand = new Command( "Exit", Command.SCREEN, 1 );
You attach the command to a canvas with the addCommand method:
canvas.addCommand( exitCommand );
You must also register a command listener using setListener :
canvas.setListener( listener );
The listener must implement the CommandListener interface. It's common for the main MIDlet class to implement CommandListener to trap exit commands, as in the following:
// Simple MIDlet
import javax.microedition.midlet.*;
public class MyMIDlet extends MIDlet implements
CommandListener {
private Display display;
private MyCanvas canvas;
private Command exitCommand = new Command(
"Exit", Command.SCREEN, 1 );
public MyMIDlet(){
display = Display.getDisplay( this );
canvas = new MyCanvas( this );
canvas.addCommand( exitCommand );
canvas.setListener( this );
}
protected void startApp(){
display.setCurrent( canvas );
}
protected void pauseApp(){
}
protected void destroyApp( boolean unconditional ){
}
public void exit(){
destroyApp( true );
notifyDestroyed();
}
public void commandAction( Command c, Displayable d ){
if( c == exitCommand ){
exit();
}
}
}
The CommandListener interface defines a single method, commandAction , that is called whenever a command is triggered. A reference to the triggered Command object is passed in as well as a reference to the display object that was active when it was triggered. (The same command can be shared across different canvases and also by the high-level API). Of course, the listener is responsible for actually carrying out the action.
Note
Sun respects your online time and privacy. The Java Developer
Connection mailing lists are used for internal Sun MicrosystemsTM purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page
(http://developer.java.sun.com/subscription/), uncheck the
appropriate checkbox, and click the Update button.
Subscribe
To subscribe to a JDC newsletter mailing list, go to the Subscriptions page (http://developer.java.sun.com/subscription/), choose the newsletters you want to subscribe to, and click Update.
Feedback
Comments? Send your feedback on the J2ME Tech Tips to: jdc-webmaster@sun.com
Archives
You'll find the J2ME Tech Tips archives at:
http://java.sun.com/jdc/J2METechTips/index.html
Copyright
Copyright 2001 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This Document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
- LINKS TO NON-SUN SITES
The J2ME Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource.
J2ME Tech Tips
March 19, 2001
Sun, Sun Microsystems, Java, Java Developer Connection, Java Embedded Server, and J2ME are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
|