用 Preferences API 存储对象

Preferences API 是轻型的、跨平台的持久性 API,它是在 JDK 1.4中引入的。它不是为了为传统数据库引擎提供一个接口,而是用恰当的、操做系统特定的后端以实现真正的持久性。这个 API 是用来存储少许数据的。事实上,它的名字自己就代表它一般用于存储用户特定的设置或者首选项,如字体大小或者窗口布局(固然,您能够在其中存储任何您想要存储的内容)。Preferences API 设计为存储字符串、数字、布尔值、简单字节数组等。在本文中,咱们将为您展现如何用 Preferences API 存储对象,并提供了一个为您处理细节的工做库。若是您的数据能够容易地表示为简单对象而不是像字符串和数字这种分离的值时,它会颇有用。咱们首先对该 API 做一简短讨论,包括一些使用它的简单例子,而后详细讨论如何使用这个 API 存储对象,并给出为咱们完成这项工做的代码。咱们还展现了一些使用这个 API 的例子。 java

若是说 Preferences API 主要是为让 Java 程序访问 Microsoft Windows 注册表而建立的,必定会让人感到意外。为何我要这么说呢?这个 API 的设计相似于 Windows 注册表,本文前三段中的大部分说明也一样适用于注册表。不过,Preferences API 就像全部 Java 语言同样,是以跨平台为目的的,因此它在非 Windows 系统上至少能够工做得同样好(固然,本文中的代码是跨平台的)。 node

Preferences API 规范没有规定如何实现这个 API,只规定了它必须作什么。Java 运行时环境(Java Runtime Environment JRE)的每个实现对这个 API 均可以有不一样的实现。许多非注册表的实现将 API 数据存储在一个 XML 格式的文件中,这个文件也许是在用户的主目录中或者在一个共享目录中。 程序员

与 Windows 注册表同样,Preferences API 使用层次树结构来存储数据。起始点是一个 root node (根节点是树的根基,全部其余节点都是这个节点的后代)。节点能够包含命名的值以及其余节点。不一样的程序将它们的数据存储在树的不一样位置上,因此它们不会彼此冲突。正如咱们将要看到的,Preferences API 采用了特殊的方法帮助防止这种冲突。 数据库

咱们将首先简单看一下 Preferences API 是如何工做的以及如何使用它。 后端

理解 Preferences API 的最好方法是使用它。须要作的第一件事是访问根节点: api

Preferences root = Preferences.userRoot();

这一行代码返回数据树的 user root。前面咱们说系统中的全部数据都存储在一个树中。不过,这并不彻底正确 -- 事实上,有 个数据树 -- 用户树和系统树。这两个树的行为彻底相同,可是它们有不一样的目的。系统树用于存储所用户均可以使用的数据,而用户树对于每个用户是不一样的。 数组

这两个树天生就有不一样的目的。您要将字体首选项存储在用户树中,由于这是用户特定的内容。另外一方面,您要将程序位置存储在系统树中,由于位置对于全部用户是相同的,而且全部用户均可能用到它。 布局

小型程序会使用系统树或者用户树,可是不会同时使用这二者。大型应用程序可能同时使用这两种树。在本文中,咱们将只针对用户树,要记住用户和系统树的行为是同样的。 字体

如今让咱们看一下如何用 Preferences API 读取和写入简单的值。 编码

当您获得根节点后,就用它读取和写入值。下面是如何写入一个字体大小:

root.putInt( "fontsize", 10 );

下面是在这以后将它读出来的方法:

int fontSize = prefs.getInt( "fontsize", 12 );

注意getInt()须要一个默认值 -- 在这里是12。

固然,您能够读取和写入整数以外的值。能够读取和写入许多基本 Java 类型。还能够将节点存储在其余节点中,如这个例子所示:

Preferences child = parent.node( "child" );

这就是 Preferences API 的所有内容 -- 剩下的就是细节使用了,咱们将在下一节讨论其中一个细节。

不难想像两个不一样的程序员可能但愿存储不一样的字体大小,若是他们决定以同一个名字“font size”存储他们的值,那么咱们就有问题了。一个程序的首选项会影响另外一个程序。

解决方法是将内容存储在包特定的位置上,像这样:

Preferences ourRoot = Preferences.userNodeForPackage( getClass() );

userNodeForPackage()方法取一个Class对象并返回这个类特定的节点。这样,每个应用程序 -- 假定它是在其本身的包中 -- 都会有本身的首选项节点。

对于 Preferences API 的工做方式有了很好的了解后,咱们还须要知道如何扩展它以便对对象进行处理。

这就是咱们但愿将对象写入 Preferences 树的理想方法:

清单 1. 将对象写入 Preferences 树的理想方法
Font font = new Font( ... );
Preferences prefs = Preferences.userNodeForPackage( getClass() );
prefs.putObject( "font", font );

不过,不幸的是,Preferences 对象没有putObject()和getObject()方法。可是咱们会尽可能作到接近这一点。咱们将在一个名为PrefObj的类中实现这些方法。如下是咱们的作法:

清单 2. 实现 putObject() 和 getObject()
Font font = new Font( ... );
Preferences prefs = Preferences.userNodeForPackage( getClass() );
PrefObj.putObject( prefs, "font", font );

咱们已经尽可能作到在Preferences类中获得一个添加方法。

下一节,咱们将看一看getObject()和putObject()是如何实现的。

咱们在这里使用的技术用到了两个技巧。第一个技巧是将对象转变为一个字节数组。这样作的缘由很简单:尽管 Preferences 对象不处理对象,可是它能够处理字节数组。

幸运的是,咱们不须要从头开始 -- 它已经创建在 Java 语言中了。有几种方式将对象转换为字节数组,下面展现了咱们在PrefObj类中是如何作的:

清单 3. 将对象转换为字节数组
static private byte[] object2Bytes( Object o ) throws IOException {
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
  ObjectOutputStream oos = new ObjectOutputStream( baos );
  oos.writeObject( o );
  return baos.toByteArray();
}

这里的关键是ObjectOutputStream类 -- 它是实际完成将对象转换为字节流这个魔术的类。经过用ObjectOutputStream包住ByteArrayOutputStream,咱们就将字节流转换为字节数组。

还有一种使用其余方式的方法:

清单 4. 将字节数组转换为对象
static private Object bytes2Object( byte raw[] )
    throws IOException, ClassNotFoundException {
  ByteArrayInputStream bais = new ByteArrayInputStream( raw );
  ObjectInputStream ois = new ObjectInputStream( bais );
  Object o = ois.readObject();
  return o;
}

必定要记ObjectOutputStream只处理实现了 java.io.Serializable 接口的对象。幸运的是,这包括了几乎全部核心 Java 库中的对象以及您的程序中全部声明为实现 Serializable 的对象。

正如我在前面提到的,Preferences API 的确能够对字节数组进行处理。不过,咱们在这里构造的字节数组并非很正确,咱们将在下一节看到这一点。

Preferences API 对能够存储在它里面的数据大小有限制。具体就是字符串限制为 MAX_VALUE_LENGTH 字符。字节数组限制为 MAX_VALUE_LENGTH 长度 75%,由于字节数组是经过编码为字符串存储的。

另外一方面,一个对象能够为任意大小,因此咱们须要将它分为几部分。固然,最容易的方法是首先将它转换为一个字节数组,而后将字节数组拆开。下面是拆开字节数组的代码,它也来自于PrefObj:

清单 5. 将字节数组拆分为可消化的大小
static private byte[][] breakIntoPieces( byte raw[] ) {
  int numPieces = (raw.length + pieceLength - 1) / pieceLength;
  byte pieces[][] = new byte[numPieces][];
  for (int i=0; i<numPieces; ++i) {
    int startByte = i * pieceLength;
    int endByte = startByte + pieceLength;
    if (endByte > raw.length) endByte = raw.length;
    int length = endByte - startByte;
    pieces[i] = new byte[length];
    System.arraycopy( raw, startByte, pieces[i], 0, length );
  }
  return pieces;
}

这里没有什么复杂的内容 -- 咱们只是建立一个数组的数组,每个长度为最大 pieceLength的字节长度(pieceLength 是 MAX_VALUE_LENGTH 的3/4)。相应地,有另外一种方法将各个部分再合并到一块儿:

清单 6. 将片断从新组装为完整的字节数组
static private byte[] combinePieces( byte pieces[][] ) {
  int length = 0;
  for (int i=0; i<pieces.length; ++i) {
    length += pieces[i].length;
  }
  byte raw[] = new byte[length];
  int cursor = 0;
  for (int i=0; i>pieces.length; ++i) {
    System.arraycopy( pieces[i], 0, raw, cursor, pieces[i].length );
    cursor += pieces[i].length;
  }
  return raw;
}

这个例程检查全部片断的总长度并建立一个具备这种长度的新数组。而后将片断一个一个地拷贝进去。

回页首

这里咱们使用第二个技巧 -- 将值转换为节点。通常来讲,当咱们用 Preferences API 存储值时,咱们将它放到首选项数据树中一个节点的 slot 中。

可是咱们不能在这里真的这样作。即便一个对象只有一个值,咱们也要将它转换为一组固定长度的字节数组。若是咱们只有一个字节数组,写入数据树中的 slot 会很容易,由于 Preferences API 直接支持字节数组。可是这行不通,由于咱们有多个数组。

技巧是为每个对象分配一个节点。让咱们弄清楚它的意义。

一般,将值存储在节点的多个 slot 的其中之一。可是咱们准备为每个对象建立一个节点, 并将字节数组存储到该节点的 slot 中。让咱们说的更具体一些。若是能够,咱们会将一个对象存储到单个 slot 中:

清单 7. 将一个对象存储到单个 slot 中
Preferences parent = ....;
parent.putObject( "child", object );

可是咱们不能这么作,由于 Preferences 没有putObject()方法。相反,咱们建立一个节点并将字节数组存储到其中,以下所示:

清单 8. 将字节数组存储到一个节点中
Preferences parent = ....;
Preferences child = parent.node( "child" );
for (int i=0; i<pieces.length; ++i) {
  child.putByteArray( ""+i, pieces[i] );
}

这样,不是将一个值存储到一个称为“child”的 slot 中,咱们将几个值存储到一个称为 “child”的节点中。这些值是用数字键 -- “0”、“1”、“2”等存储的。

使用数字键可使后面读取片断时更容易:

清单 9. 读取容易读的片断
Preferences parent = ....;
Preferences child = parent.node( "child" );
for (int i=0; i<numPieces; ++i) {
  pieces[i] = child.getByteArray( ""+i, null );
}

在下一节,咱们将看一下结合全部这些步骤的例程。

回页首

PrefObjs有一个名为putObject()的静态方法,它调用在前面清单 358 中描述的方法。其内容以下:

清单 10. 方法 putObject() 使用其余方法来写入片断
static public void putObject( Preferences prefs, String key, Object o )
    throws IOException, BackingStoreException, ClassNotFoundException {
  byte raw[] = object2Bytes( o );
  byte pieces[][] = breakIntoPieces( raw );
  writePieces( prefs, key, pieces );
}

方法putObject()将整个过程分为三步,分别嵌入咱们在前面讨论过的三个方法。它将对象转换为字节数组( 清单 3)、将数组分解为更小的数组( 清单 5)、而后将片断写入 Preferences API。

有一个用于读取的相似方法:

清单 11. 方法 getObject() 对写入片断作一样的事情
static public Object getObject( Preferences prefs, String key )
    throws IOException, BackingStoreException, ClassNotFoundException {
  byte pieces[][] = readPieces( prefs, key );
  byte raw[] = combinePieces( pieces );
  Object o = bytes2Object( raw );
  return o;
}

这个方法从 Preferences API 中读取片断,并将它们结合为单个字节数组,而后将它转换为对象。

正如您所看到的,这是一种使用 Preferences API 所具备的功能的简洁方式,实现了它 原本不具有的功能。这是一种扩展示有库的好方法。理论上,您能够改变库或者建立子类,可是这样有可能会干扰其余使用 Preferences API 的程序。而使用这种方式,您能够保持原来的 API 不变,同时以一种干净、有用的方式扩展它。
相关文章
相关标签/搜索