Preferences API是JDK 1.4中引入的轻量级跨平台持久性API。 它并不是要与传统的数据库引擎接口,而是要使用特定于OS的后端来实现实际的持久性。 该API用于存储少量数据。 实际上,该名称本身就表明了这一点:它的通常用法是存储用户特定的设置或首选项,例如字体大小或窗口布局。 (当然,您可以在其中存储任何内容。)
Preferences API旨在存储字符串,数字,布尔值,简单字节数组等。 在本文中,我们将向您展示如何使用Preferences API存储对象,并提供一个工作库来为您处理细节。 如果您的数据很容易表示为简单对象,而不是诸如字符串和数字之类的单独值,则此功能很有用。
我们将从关于API的简短讨论开始,包括使用它的一些简单示例,然后详细介绍如何使用API存储对象,并为我们安排代码。 我们还将演示一些实际使用的API的示例。
为什么要设计首选项API?
如果创建Preferences API主要是为了允许Java程序访问Microsoft Windows注册表,那将是令人惊讶的。 我为什么要提这个? API的设计类似于Windows注册表的设计。 本文前三段中的大多数语句同样适用于注册表。
但是,首选项API与所有Java语言一样,都是跨平台的,因此至少在非Windows系统上也能正常工作。 (当然,本文中的代码是跨平台的。)
首选项API的规范并未指定必须如何实现API,而仅指定了必须执行的操作。 Java运行时环境(JRE)的每种实现都可以不同地实现API。 许多非注册表实现将API数据存储在用户主目录或共享目录中的XML格式的文件中。
像Windows注册表一样,首选项API使用分层树隐喻来存储数据。 起点是根节点 (根节点是树的基础;其他所有节点都是该节点的后代)。 节点可以包含命名值,也可以包含其他节点。 不同的程序将其数据存储在树的不同部分,因此它们不会相互冲突。 正如我们将看到的那样,Preferences API采取了特殊的措施来帮助防止此类冲突。
首先,我们将快速浏览Preferences API的工作方式和使用方式。
使用偏好
了解Preferences API的最佳方法是使用它。 您需要做的第一件事是获得对根节点的访问权:
Preferences root = Preferences.userRoot();
此行代码返回数据树的用户根 。 之前,我们说过系统中的所有数据都存储在树中。 嗯,这不是完全正确的-实际上,有两个数据树-用户树和系统树。 这两棵树的行为相同,但是用途不同。 系统树用于存储旨在供所有用户使用的数据,而每个用户的用户树都不相同。
这两棵树自然有不同的用途。 您将字体首选项存储在用户树中,因为这是特定于用户的问题。 另一方面,您将程序的位置存储在系统树中,因为该位置对于所有用户都是相同的,并且可能对所有用户有用。
较小的程序将使用系统树或用户树,但不会同时使用两者。 较大的应用程序可能会同时使用。 在本文中,我们将只处理用户树,请记住用户树和系统树的行为相同。
现在,让我们看看如何使用Preferences API读取和写入简单值。
获得价值
拥有根节点之后,就可以使用它来读取和写入值。 这是您写字体大小的方法:
root.putInt( "fontsize", 10 );
以下是您稍后阅读的方式:
int fontSize = prefs.getInt( "fontsize", 12 );
请注意, getInt()
需要一个默认值-在这种情况下为12。
当然,您不仅可以读写整数。 您可以读写许多原始Java类型。 您还可以将节点存储在其他节点中,如本示例所示:
Preferences child = parent.node( "child" );
这就是Preferences API的全部内容–其余的有用之处在于细节,我们将在下一部分中介绍其中之一。
获取包的节点
不难想象,两个不同的程序员可能想存储不同的字体大小,如果他们每个人都决定以“字体大小”的名称存储它们,那么我们就会遇到问题。 一个程序的首选项会感染另一个程序。
解决方案是将内容存储在特定于程序包的位置,如下所示:
Preferences ourRoot = Preferences.userNodeForPackage( getClass() );
userNodeForPackage()
方法采用Class
对象,并返回特定于该类的节点。 这样,每个应用程序(大概在自己的程序包中)将具有自己的首选项节点。
现在我们对Preferences API的工作原理有了一个很好的了解,我们需要知道如何扩展它以能够处理对象。
储存物件
理想情况下,这就是我们希望能够将对象写入“首选项”树的方式:
清单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的四分之三)。 相应地,还有另一种方法可以将各个部分重新组合在一起:
清单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;
}
此例程检查片段的总长度,并创建一个新的单个数组,该数组是如此之长。 然后,它一个接一个地复制片段。
读和写作品
在此阶段,我们采用第二个技巧-将值转换为节点。 通常,当我们使用首选项API存储值时,会将其放在首选项数据树中的一个节点中的插槽中。
但是我们不能在这里真正做到这一点。 即使对象是单个值,我们也已将其转换为一组固定长度的字节数组。 如果我们只有一个字节数组,那么将很容易写入数据树中的插槽,因为Preferences API直接支持字节数组。 但这不起作用,因为我们有多个数组。
诀窍是为每个对象分配一个节点。 让我们确保我们清楚这意味着什么。
通常,将值存储在节点中其他插槽之间存在的插槽中。 但是,我们将为每个对象创建一个节点,并将字节数组存储在该节点的插槽中。 让我们将其具体化。 如果可以的话,我们可以将一个对象存储到一个插槽中:
清单7.将对象存储在一个插槽中
Preferences parent = ....;
parent.putObject( "child", object );
但是我们不能这样做,因为“首选项”没有putObject()
方法。 相反,我们创建一个节点并将字节数组存储在其中,如下所示:
清单8.相反,将字节数组存储在节点中
Preferences parent = ....;
Preferences child = parent.node( "child" );
for (int i=0; i<pieces.length; ++i) {
child.putByteArray( ""+i, pieces[i] );
}
因此,我们不是将多个值存储在称为“子”的插槽中,而是将多个值存储在名为“子”的节点中。 这些值使用数字键“ 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()
它调用方法中编目前面所述3 , 5 ,和8 。 看起来像这样:
清单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完整无缺,同时以一种干净,有用的方式扩展它。
翻译自: https://www.ibm.com/developerworks/java/library/j-prefapi/index.html