6.JAVA NIO 字符集

第六章 字符集

我们生活在一个变化莫测的世界中。甚至在这个我们称之为地球的平凡的 M 级行星 上,我们也使用数百种不同的语言。在《The Hitchhikers Guide to the Galaxy》(即《银河系漫游指南》)中,Arthur Dent 把 Babelfish(宝贝鱼)放在耳朵里,从而解决了他的语言 问题。之后在他偶然的银河旅行 1 中,他就可以理解所遇到的由不同字符(至少可以说) 组成的语言。

在 Java 平台上,我们没有奢侈的 Babelfish 技术(至少现在没有)2 ,但我们仍必须处 理多种语言以及组成这些语言的多个字符。幸运的是,Java 是第一个被广泛使用的编程语 言,它使用内在的 Unicode 来表示字符。与以字节为导向的编程语言例如 C 或 C++相比, Unicode 的固有支持大大的简化了字符数据处理,但决不是自动的处理字符。您仍需要理 解字符映射的工作原理以及如何处理多个字符集。

1.字符集基础

在讨论 java.nio.charsets 中新类的细节之前,让我们先来定义一些与字符集和 字符代码转换相关的术语。新的字符集类表示到该领域的更标准化的方法,所以明确术语 的使用是很重要的。

Character set(字符集)

字符的集合,也就是,带有特殊语义的符号。字母“A”是一个字符。“%”也 是一个字符。没有内在数字价值,与 ASCII,Unicode,甚至是电脑也没有任何的 直接联系。在电脑产生前的很长一段时间内,符号就已经存在了。

Coded character set(编码字符集)

一个数值赋给一个字符的集合。把代码赋值给字符,这样它们就可以用特定的字 符编码集表达数字的结果。其他的编码字符集可以赋不同的数值到同一个字符 上。字符集映射通常是由标准组织确定的,例如 USASCII,ISO 8859-1,Unicode (ISO 10646-1),以及 JIS X0201。

Character-encoding scheme(字符编码方案)

编码字符集成员到八位字节(8 bit 字节)的映射。编码方案定义了如何把字符编 码的序列表达为字节序列。字符编码的数值不需要与编码字节相同,也不需要是 一对一或一对多个的关系。原则上,把字符集编码和解码近似视为对象的序列化 和反序列化。

 

通常字符数据编码是用于网络传输或文件存储。编码方案不是字符集,它是映射;但是因为它们之间的紧密联系,大部分编码都与一个独立的字符集相关联。例如,UTF-8, 仅用来编码 Unicode 字符集。尽管如此,用一个编码方案处理多个字符集还是可能发生 的。例如,EUC 可以对几个亚洲语言的字符进行编码。

图 6-1 是使用 UTF-8 编码方案将 Unicode 字符序列编码为字节序列的图形表达式。UTF-8 把小于 0x80 的字符代码值编码成一个单字节值(标准 ASCII)。所有其他的 Unicode 字 符都被编码成 2 到 6 个字节的多字节序列(http://www.ietf.org/rfc/rfc2279.txt)。

Charset(字符集)

术语 charset 是在 RFC2278(http://ietf.org/rfc/rfc2278.txt)中定义的。它是编码字符集 和字符编码方案的集合。java.nio.charset 包的锚类是 Charset,它封装字符 集抽取。

Unicode 是 16-位字符编码。它试着把全世界所有语言的字符集统一到一个独立的、全面的映射中。它赢得了一席之地,但是目前仍有许多其他字符编码正在被广泛的使用。 大部分的操作系统在 I/O 与文件存储方面仍是以字节为导向的,所以无论使用何种编码, Unicode 或其他编码,在字节序列和字符集编码之间仍需要进行转化。

由 java.nio.charset 包组成的类满足了这个需求。这不是 Java 平台第一次处理字 符集编码,但是它是最系统、最全面、以及最灵活的解决方式。java.nio.charset.spi 包提供服务器供给接口(SPI),使编码器和解码器可以根据 需要选择插入。

2.字符集

到 JDK1.4 为止,每个 JVM 实现都需要支持标准字符集,表 6-1 列出了具体内容。 JVM 实现可以随意的支持额外的字符集,但是必须提供这个最大集。(以您的 JVM 版本 文件为参考,确定额外的字符集是否可用。)注意,尽管所有的 JVM 必须至少支持下列字 符集,但是没有指定默认字符集,也没有要求是这些标准字符集之一。在 JVM 启动时确定 默认值,取决于潜在的操作系统环境、区域设置、和/或 JVM 配置。如果您需要一个指定 的字符集,最安全的办法是明确的命名它。不要假设默认部署与您的开发环境相同。

字符集名称不区分大小写,也就是,当比较字符集名称时认为大写字母和小写字母相同。

互联网名称分配机构(IANA)维护所有正式注册的字符集名称,而表 6-1 中列出的所有名 称都是在 IANA 注册的标准名称。

UTF-16BE 和 UTF-16LE 把每个字符编码为一个 2-字节数值。因此这类编码的解码器必须 要预先了解数据是如何编码的,或者根据编码数据流本身来确定字节顺序的方式。UTF-16 编码承认一种字节顺序标记:Unicode 字符\uFEFF。只有发生在编码流的开端时字节顺序 标记才表现为其特殊含义。如果之后遇到该值,它是根据其定义的 Unicode 值(零宽度, 无间断空格)被映射。外来的,小字节序系统可能会优先考虑\uFEF 并且把流编码为 UTF-16LE。使用 UTF-16 编码优先考虑和认可字节顺序标记使系统带有不同的内部字节顺 序,从而与 Unicode 数据交流。

表 6-2 解释了 Java 平台针对不同的组合所采取的操作。

示例 6-1 演示了通过不同的 Charset 实现如何把字符翻译成字节序列。

示例 6-1.使用标准字符集编码

package org.example;

import java.nio.ByteBuffer;
import java.nio.charset.Charset;

/**
 * Charset encoding test. Run the same input string, which contains
 * some non-ascii characters, through several Charset encoders and dump out
 * the hex values of the resulting byte sequences.
 *
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class EncodeTest {

    public static void main(String[] argv) throws Exception {
        // This is the character sequence to encode
        String input = "\u00bfMa\u00f1ana?";
        // the list of charsets to encode with
        String[] charsetNames = {
                "US-ASCII", "ISO-8859-1",
                "UTF-8", "UTF-16BE",
                "UTF-16LE", "UTF-16" // , "X-ROT13"
        };
        for (int i = 0; i < charsetNames.length; i++) {
            doEncode(Charset.forName(charsetNames[i]), input);
        }
    }

    /**
     * For a given Charset and input string, encode the chars
     * and print out the resulting byte encoding in a readable form.
     */
    private static void doEncode(Charset cs, String input) {
        ByteBuffer bb = cs.encode(input);
        System.out.println("Charset: " + cs.name());
        System.out.println(" Input: " + input);
        System.out.println("Encoded: ");
        for (int i = 0; bb.hasRemaining(); i++) {
            int b = bb.get();
            int ival = ((int) b) & 0xff;
            char c = (char) ival;
            // Keep tabular alignment pretty
            if (i < 10) System.out.print(" ");
            // Print index number
            System.out.print(" " + i + ": ");
            // Better formatted output is coming someday...
            if (ival < 16) System.out.print("0");
            // Print the hex value of the byte
            System.out.print(Integer.toHexString(ival));
            // If the byte seems to be the value of a
            // printable character, print it. No guarantee
            // it will be.
            if (Character.isWhitespace(c) || Character.isISOControl(c)) {
                System.out.println("");
            } else {
                System.out.println(" (" + c + ")");
            }
        }
        System.out.println("");
    }
}

下面是运行 EncodeTest 的输出结果:

Charset: US-ASCII
 Input: ¿Mañana?
Encoded: 
  0: 3f (?)
  1: 4d (M)
  2: 61 (a)
  3: 3f (?)
  4: 61 (a)
  5: 6e (n)
  6: 61 (a)
  7: 3f (?)

Charset: ISO-8859-1
 Input: ¿Mañana?
Encoded: 
  0: bf (¿)
  1: 4d (M)
  2: 61 (a)
  3: f1 (ñ)
  4: 61 (a)
  5: 6e (n)
  6: 61 (a)
  7: 3f (?)

Charset: UTF-8
 Input: ¿Mañana?
Encoded: 
  0: c2 (Â)
  1: bf (¿)
  2: 4d (M)
  3: 61 (a)
  4: c3 (Ã)
  5: b1 (±)
  6: 61 (a)
  7: 6e (n)
  8: 61 (a)
  9: 3f (?)

Charset: UTF-16BE
 Input: ¿Mañana?
Encoded: 
  0: 00
  1: bf (¿)
  2: 00
  3: 4d (M)
  4: 00
  5: 61 (a)
  6: 00
  7: f1 (ñ)
  8: 00
  9: 61 (a)
 10: 00
 11: 6e (n)
 12: 00
 13: 61 (a)
 14: 00
 15: 3f (?)

Charset: UTF-16LE
 Input: ¿Mañana?
Encoded: 
  0: bf (¿)
  1: 00
  2: 4d (M)
  3: 00
  4: 61 (a)
  5: 00
  6: f1 (ñ)
  7: 00
  8: 61 (a)
  9: 00
 10: 6e (n)
 11: 00
 12: 61 (a)
 13: 00
 14: 3f (?)
 15: 00

Charset: UTF-16
 Input: ¿Mañana?
Encoded: 
  0: fe (þ)
  1: ff (ÿ)
  2: 00
  3: bf (¿)
  4: 00
  5: 4d (M)
  6: 00
  7: 61 (a)
  8: 00
  9: f1 (ñ)
 10: 00
 11: 61 (a)
 12: 00
 13: 6e (n)
 14: 00
 15: 61 (a)
 16: 00
 17: 3f (?)

1)字符集类

让我们深入到 Charset 类的 API 中(用图 6-2 概括说明):

package java.nio.charset;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Locale;
import java.util.Set;
import java.util.SortedMap;

public abstract class Charset implements Comparable<Charset> {

    public static boolean isSupported(String charsetName)

    public static Charset forName(String charsetName)

    public static SortedMap<String, Charset> availableCharsets()

    public final String name()

    public final Set<String> aliases()

    public String displayName()

    public String displayName(Locale locale)

    public final boolean isRegistered()

    public boolean canEncode()

    public abstract CharsetEncoder newEncoder();

    public final ByteBuffer encode(CharBuffer cb)

    public final ByteBuffer encode(String str)

    public abstract CharsetDecoder newDecoder();

    public final CharBuffer decode(ByteBuffer bb)

    public abstract boolean contains(java.nio.charset.Charset cs);

    public final boolean equals(Object ob)

    public final int compareTo(Charset that)

    public final int hashCode()

    public final String toString()
}

Charset 类封装特定字符集的永恒信息。Charset 是抽取。通过调用静态工厂方法 forName()获得具体实例,导入所需字符集的名称。所有的 Charset 方法都是线程安全的; 单一实例可以在多个线程中共享。

可以调用布尔类(boolean class)方法 isSupported()来确定在 JVM 运行中当前指定的 字符集是否可用。通过 Charset SPI 机制可以动态安装新的字符集,所以给定字符集名称的 答案可以随时间变化。在第 6.3 节中论述了 Charset SPI。

一个字符集可以有多个名称。通常它有一个规范名称但是也有零个或多个别名。规范 名称或别名都可以通过 forName()和 isSupported()进行使用。

一些字符集也有历史遗留的名称,它们用于之前的 Java 平台版本并且向后兼容。字符集的历史名称是由 InputStreamReader 和 OutputStream-Writer 类的 getEncoding()返回。如果 字符集有历史名称,那么它将是规范名称或者 Charset 的别名之一。Charset 类不提供指出 历史名称的标示。

静态类方法的最后一个,availableCharsets(),将返回在 JVM 中当前有效的所有字符集 的 java.util.SortedMap。正如 isSupported(),如果安装新的字符集返回的值会随着时间改 变。返回映射的成员将是用它们的规范名称作为密钥的 Charset 对象。迭代时,映射将根 据规范名称按字母顺序排列。

availableCharsets()方法可能很慢。虽然支持许多字符集,但是通常它们只在有明确要 求时才能被创建。调用 availableCharsets()需要实例化所有已知的 Charset 对象。实例化 Charset 可能需要加载库、网络资源访问、翻译表计算等等。如果您知道要使用的字符集的 名称,使用 forName()方法。当您需要列举所有可用的字符集时使用 availableCharsets()例如,面向交互用户展示一个选项。假设在此期间未安装新的字符集,通过 availableCharsets()返回的 Map 精确的包含了用 forName()可返回的相同的字符集。

一旦获取 Charset 实例的参数,name()方法将返回字符集的规范名称,并且 aliases()将 给出包含别名的 Set。由 aliases()返回的 Set 将永远不会是 null,但是可以为空。

每个 Charset 对象也有两个 displayName()方法。这些方法的默认实现仅仅返回规范字 符集名称。这些方法可以提供本地化的显示名称,例如,用在菜单或选项中。 displayName()方法可以使用 Locale 参数指定一个地方化的环境。无参数版本使用默认区域 设置。

如本节开端提到的,IANA 是维护字符集名称的权威登记机构。如果给出的 Charset 对 象表示在 IANA 注册的字符集,那么 isRegistered()方法将返回 true。如果是这样的话,那 么 Charset 对象需要满足几个条件:

  • 字符集的规范名称应与在 IANA 注册的名称相符。

  • 如果 IANA 用同一个字符集注册了多个名称,对象返回的规范名称应该与 IANA 注册 中的 MIME-首选名称相符。

  • 如果字符集名称从注册中移除,那么当前的规范名称应保留为别名。

  • 如果字符集没有在 IANA 注册,它的规范名称必须以“X-”或“x-”开头。

 

大多数情况下,只有 JVM 卖家才会关注这些规则。然而,如果您打算以您自己的字符 集作为应用的一部分,那么了解这些不该做的事情将对您很有帮助。针对 isRegistered()您 应该返回 false 并以“X-”开头命名您的字符集。见 6.3 节。

2)字符集比较

下面的代码中包含了我们将在本节中详述的 Charset 的 API 方法:

package java.nio.charset;

public abstract class Charset implements Comparable<Charset> {

    // This is a partial API listing
    public abstract boolean contains(java.nio.charset.Charset cs);

    public final boolean equals(Object ob)

    public final int compareTo(Charset that)

    public final int hashCode()

    public final String toString()
}

回想一下,字符集是由字符的编码集与该字符集的编码方案组成的。与普通的集合类 似,一个字符集可能是另一个字符集的子集。一个字符集(C1 )包含另一个(C2 ),表示 在 C 2 中表达的每个字符都可以在 C 1 中进行相同的表达。每个字符集都被认为是包含其本 身。如果这个包含关系成立,那么您在 C2 (被包含的子集)中编码的任意流在 C 1 中也一 定可以编码,无需任何替换。

contains()实例方法显示作为参数传入的 Charset 对象是否被该 Charset 对象封装的字符集所包含。该方法不能在运行时动态比较字符集;只有当具体的 Charset 类确定给出的字 符集被包含的情况下才返回 true。如果 contains()返回 false,表示包含关系不存在或未 知的包含关系。

如果一个字符集被另一个包含,这不意味着产生的编码字节序列将会等同于给定的输 入字符序列。

Charset 类明确地覆盖了 Object.equals()方法。如果 Charset 的实例拥有相同的规范名称 (由 name()返回),它们就被认为是相同的。在 JDK1.4.0 版本中,由 equals()实现的比较 是规范名称串的简单比较,这意味着在测试过程中区分大小写。这是在未来的版本中应该 更正的错误程序。由于 Charset.equals()方法覆盖了 Object 类中的默认方法,它必须声明为 接受一个 Object 类的参数,而不是 Charset 类。Charset 类的对象永远不会与其他任意类的 对象相等。

您可能注意到了之前的代码清单中 Charset 实现了 Comparable 接口,表示它提供 compareTo() 方法。与 equals()类似的, compareTo()以 Charset 对象的规范名称为基础,返回结果。用 Charset 的 compareTo()方法进行比较时大小写不计。如果对 Charset 对象的集合进行排序,它们将按规范名称 的顺序排列,大小写不计。再一次的,因为在 Comparable 中定义的 compareTo()方法采用 Object 作 为参数类型,所以此处定义的这个也一样。如果您使用非 Charset 对象作为 compareTo()方法的参 数,将产生 ClassCastException。compareTo()不能比较不同类型的对象实例。

继续用它们的规范名称鉴别 Charset 对象,hashCode()方法返回由 name()方法(意思是散列代 码(也叫哈希代码)区分大小写)返回的 String 的散列代码。Charset 的 toString()方法返回规范名 称。大部分时间,hashCode()的实现和 toString()方法不起什么作用。在这里提到它们是因为 Charset 类覆盖它们,如果在散列映射中使用可能会影响它们的表现或者在调试器中影响它们的出 现方式。

现在我们已经学习了简单的 API 方法,让我们来看一下字符集编码器。这是字符和字节流之间 实际完成转化的地方。

 

3)字符集编码器

字符集是由一个编码字符集和一个相关编码方案组成的。CharsetEncoder 和 CharsetDecoder 类 实现转换方案。(见图 6-1.)

package java.nio.charset;

public abstract class Charset implements Comparable<Charset> {

    // This is a partial API listing
    public abstract boolean contains(java.nio.charset.Charset cs);

    public final boolean equals(Object ob)

    public final int compareTo(Charset that)

    public final int hashCode()

    public final String toString()
}

这里有用的第一个 API 方法是 canEncode()。该方法表示这个字符集是否允许编码。几乎所有 的字符集都支持编码。主要的例外情况是带有解码器的字符集,它们可以自检测字节序列是如何编 码并且之后会选择一个合适的解码方案。这些字符集通常只支持解码并且不创建自己的编码。

如果该 Charset 对象能够编码字符序列,canEncode()方法返回 true。如果为 false,上面列 出的其他三个方法不应该在那个对象上被调用。这样做将引发 UnsupportedOperationException。

调用 newEncoder()返回 CharsetEncoder 对象,可以使用和字符集相关的编码方案把字符序列转 化为字节序列。之后我们将在本节中学习 CharsetEncoder 类的 API,但是首先我们要快速浏览一下 Charset 余下的两个方法。

Charset 的两个 encode()方法使用方便,用默认值针对和字符集相关的编码器实现编码。两个都 返回新 ByteBuffer 对象,包含符合给定的 String 或 CharBuffer 字符的一个编码字节序列。解码器通 常都在 CharBuffer 对象上运行。encode()的形式采用 String 参数自动的为您创建一个临时的 CharBuffer,等同于下面这个:

charset.encode (CharBuffer.wrap (string));

在 Charset 对象上调用 encode()使用编码器的默认设置,等同于下列代码:

		在 Charset 对象上调用 encode()使用编码器的默认设置,等同于下列代码:

之后我们将进行讨论,它运行解码器,用默认字节序列替换任意的未识别或无效输入字符。

让我们针对 CharsetEncoder 浏览一下 API,从而更全面的理解字符编码的过程:

package java.nio.charset;

import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

public abstract class CharsetEncoder {

    public final Charset charset()

    public final float averageBytesPerChar()

    public final float maxBytesPerChar()

    public final CharsetEncoder reset()

    public final ByteBuffer encode(CharBuffer in) throws CharacterCodingException

    public final CoderResult encode(CharBuffer in, ByteBuffer out, boolean endOfInput)

    public final CoderResult flush(ByteBuffer out)

    public boolean canEncode(char c)

    public boolean canEncode(CharSequence cs)

    public CodingErrorAction malformedInputAction()

    public final CharsetEncoder onMalformedInput(CodingErrorAction newAction)

    public final CharsetEncoder onUnmappableCharacter(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final byte[] replacement()

    public boolean isLegalReplacement(byte[] repl)

    public final CharsetEncoder replaceWith(byte[] newReplacement)
}

CharsetEncoder 对象是一个状态转换引擎:字符进去,字节出来。一些编码器的调用可能需要 完成转换。编码器存储在调用之间转换的状态。

这里列出的首个方法组提供跟 CharsetEncoder 对象有关的永恒信息。每个编码器和一个 Charset 对象相关联,而 charset()方法返回一个备份参考。

averageBytesPerChar()方法返回一个浮点值,表示编码集合的字符所需的平均字节数量。注意 该值可以是分数值。当编码字符时,编码运算法则可以选择调节字节边界,或者一些字符可以编码 成大于其他字节的字节(UTF-8 就是这样工作的)。该方法作为一个程序的探索很有用,用来确定 ByteBuffer 的近似尺寸,ByteBuffer 需要包含给定字符的编码字节。

最后,maxBytesPerChar()方法表示在集合中编码单字符需要的最大字节数。这也是一个浮点 值。与 averageBytesPerChar()类似,该方法被用来按大小排列 ByteBuffer。用 maxBytesPerChar()返 回的值乘以被编码的字符数量将得出最坏情况输出缓冲区大小。

在我们进入编码的要点之前,关于 CharsetEncoder API 的一个注意事项:首先,越简单的encode()形式越方便,在重新分配的 ByteBuffer 中您提供的 CharBuffer 的编码集所有的编码于一 身。这是当您在 Charset 类上直接调用 encode()时最后调用的方法。

当使用 CharsetEncoder 对象时,在编码之前或编码期间有设置错误处理参数的选项。(本节的 后半段将讨论处理编码错误。)调用 encode()的单参数形式实现完整的编码循环(复位,编码以及 清理),所以编码器之前的内状态将丢失。

让我们详细了解一些编码处理的工作原理。CharsetEncoder 类是一个状态编码引擎。实际上, 编码器有状态意味着它们不是线程安全的:CharsetEncoder 对象不应该在线程中共享。编码可以在 一个简单的步骤中完成,如上面提到的 encode()的首个形式,或者重复调用 encode()的第二个形 式。编码过程如下:

  1. 通过调用 reset()方法复位编码器的状态。 让编码引擎准备开始产生编码字节流。 新建的 CharsetEncoder 对象不需要复位,但是这么做也无妨。

  2. 不调用或多次调用 encode()为编码器提供字符,endOfnput 参数 false 表示后面可能有更多 的字符。给定的 CharBuffer 将消耗字符,而编码字节序列将被添加到提供的 ByteBuffer 上。返回时,输入 CharBuffer 可能不是全部为空。可能填入输出 ByteBuffer,或者编码器可能需要 更多的输入来完成多字符转化。编码器本身可能也保留可以影响序列转化实现的状态。在重新 填入前紧凑输入缓冲区。

  3. 最后一次调用 encode(),针对 endOfInput 参数导入 true。提供的 CharBuffer 可能包含额外 的需要编码的字符或为空。重要的是 endOfInput 在最后的调用上为 true。这样就通知编码 引擎后面没有输入了,允许它探测有缺陷的输入。

  4. 调用 flush()方法来完成未完成的编码并输出所有剩下的字节。如果在输出 ByteBuffer 中没有足 够的空间,需要多次调用该方法。

 

当消耗了所有的输入时,当输出 ByteBuffer 为满时,或者当探测到编码错误时,encode()方法 返回。无论如何,将会返回 CoderResult 对象,来表示发生的情况。结果对象可表示下列结果条件 之一:

Underflow(下溢)

正常情况,表示需要更多的输入。或者是输入 CharBuffer 内容不足;或者,如果它不为 空,在没有额外的输入的情况下,余下的字符无法进行处理。更新 CharBuffer 的位置解决 被编码器消耗的字符的问题。

在 CharBuffer 中填入更多的编码字符(首先在缓冲区上调用 compact(),如果是非空的情 况)并再次调用 encode()继续。如果结束了,用空 CharBuffer 调用 encode()并且 endOfInput 为 true,之后调用 flush()确保所有的字节都被发送给 ByteBuffer。

下溢条件总是返回相同的对象实例:名为 CharsetEncoder.UNDERFLOW 的静态类变量。 这就使您可以使用返回的对象句柄上的等号运算符(==)来对下溢进行检测。

Overflow(上溢)

表示编码器充满了输出 ByteBuffer 并且需要产生更多的编码输出。输入 CharBuffer 对象可能会或可能不会被耗尽。这是正常条件,不表示出错。您应该消耗 ByteBuffer 但是不应该 扰乱 CharBuffer,CharBuffer 将更新它的位置,之后再次调用 encode()。重复进行直到得 到下溢结果。

与下溢类似的,上溢返回一致的实例,CharsetEncoder.OVERFLOW,它可直接用于等 式比较。

Malformed input(有缺陷的输入)

编码时,这个通常意味着字符包含 16-位的数值,不是有效的 Unicode 字符。对于解码来 说,这意味着解码器遭遇了不识别的字节序列。

返回的 CoderResult 实例将不是单一的参数,因为它是针对下溢和上溢的。见第 6.2.3.1 节 CoderResult 的 API。

Unmappable character(无映射字符)

表示编码器不能映射字符或字符的序列到字节上—例如,如果您正在使用 ISO-8859-1 编 码但您的输入 CharBuffer 包含非-拉丁 Unicode 字符。对于解码,解码器知道输入字节序 列但是不了解如何创建相符的字符。

 

编码时,如果编码器遭遇了有缺陷的或不能映射的输入,返回结果对象。您也可以检测独立的 字符,或者字符序列,来确定它们是否能被编码。下面是检测能否进行编码的方法:

package java.nio.charset;

public abstract class CharsetEncoder {

    public boolean canEncode(char c)

    public boolean canEncode(CharSequence cs)
}

canEncode()的两个形式返回 boolean 结果,表示编码器是否能将给出的输入编码。两种方法 都在一个临时的缓冲区内实现输入的编码。这将引起编码器内部状态的改变,所以当编码处理正在 进行中时不应调用这些方法。开始编码处理前,使用这些方法检测您的输入。

canEncode()的第二个形成采用类型 CharSequence 的一个参数,在第五章介绍过。任何实现 CharSequence(当前 CharBuffer, String, 或 StringBuffer)的对象都可以导入到 canEncode()中。

CharsetEncoder 的剩下的方法包含在处理编码错误中:

package java.nio.charset;

import java.nio.charset.CodingErrorAction;

public abstract class CharsetEncoder {

    public CodingErrorAction malformedInputAction()

    public final CharsetEncoder onMalformedInput(CodingErrorAction newAction)

    public final CharsetEncoder onUnmappableCharacter(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final byte[] replacement()

    public boolean isLegalReplacement(byte[] repl)

    public final CharsetEncoder replaceWith(byte[] newReplacement)
}

如之前提到的,CoderResult 对象可以从 encode()中返回,表示编码字符序列的问题。有两个已 定义的代码错误条件:malformed 和 unmappable。在每一个错误条件上都可以配置编码器实例来采 取不同的操作。当这些条件之一发生时,CodingErrorAction 类封装可能采取的操作。 CodingErrorAction 是无有用的方法的无价值的类。它是简单的,安全类型的列举,包含了它本身的 静态、已命名的实例。CodingErrorAction 定义了三个公共域:

REPORT(报告)

创建 CharsetEncoder 时的默认行为。这个行为表示编码错误应该通过返回 CoderResult 对象 报告,前面提到过。

IGNORE(忽略)

表示应忽略编码错误并且如果位置不对的话任何错误的输入都应中止。

REPLACE(替换)

通过中止错误的输入并输出针对该 CharsetEncoder 定义的当前的替换字节序列处理编码错误。

 

现在我们知道了可能的错误行为,如何使用之前的 API 清单中的前四个方法应该是相当明显 的。malformedInputAction()方法返回针对有缺陷的输入生效的行为。调用 onMalformedInput()设置在 那之后要使用的 CodingErrorAction 值。无法映射字符的一对类似方法设置错误行为,并返回 CharsetEncoder 对象句柄。通过返回 CharsetEncoder,这些方法允许调用链接。例如:

        CharsetEncoder encoder = charset.newEncoder()
                .onMalformedInput(CodingErrorAction.REPLACE)
                .onUnmappableCharacter(CodingErrorAction.IGNORE);

当动作是 CodingErrorAction.REPLACE 时,在 CharsetEncoder 上的方法的最后一组处理 要使用的替换序列的管理。

当前的替换字节序列可以通过调用 replacement()方法找回。如果您未设置自己的替换序列,将 返回默认值。

您可以通过调用 isLegalReplacement()方法,用您想要使用的字节阵列检测替换序列的合法性。 替换字节序列必须是对字符集有效的编码。记住,字符集编码把字符转化成字节序列,为以后的解 码做准备。如果替换序列不能被解码成有效的字符序列,编码字节序列变为无效。

最后,您可以通过调用 replaceWith()设置新的替换序列并导入字节阵列。当编码错误发生时, 将输出给定的字节序列并且相应的错误行为被设置为 CodingErrorAction.REPLACE。阵列中 的字节序列必须是合法的替换值:如果不是,java.lang.IllegalArgumentException 将被抛出。返回值 是 CharsetEncoder 对象本身。

CoderResult类

让我们看一下前面提到的 CoderResult 类。CoderResult 对象是由 CharsetEncoder 和 CharsetDecoder 对象返回的:

package java.nio.charset;

import java.nio.charset.CharacterCodingException;

public class CoderResult {

    public static final CoderResult OVERFLOW
    public static final CoderResult UNDERFLOW

    public boolean isUnderflow()

    public boolean isOverflow()

    public boolean isError()

    public boolean isMalformed()

    public boolean isUnmappable()

    public int length()

    public static CoderResult malformedForLength(int length)

    public static CoderResult unmappableForLength(int length)

    public void throwException() throws CharacterCodingException
}

如前面提到的,根据每个下溢和上溢条件返回 CoderResult 的一致实例。您可以看到在 CoderResult 类中,上述这些定义为公共静态域。这些实例可以使通用案例的检测变得更容易。您 可以通过==运算符直接把 CoderResult 对象和这些公共域进行比较(见示例 6-2)。无论哪个 CoderResult 对象,您总是可以使用 API 来确定返回结果的含义。

前两个方法,isUnderflow()和 isOverflow(),不被认定是错误。如果其中一个方法返回 true, 那么从 CoderResult 对象中不能再获取更多的信息。对于 isUnderflow(),CoderResult.UNDERFLOW 实例总是返回 true,对于 isOverflow(),CoderResult.OVERFLOW 总是返回 true。

其他两个布尔(boolean)函数,isMalformed()和 isUnmappable(),是错误条件。isError()方法是简 便方法,如果其中一个方法返回 true 这个方法就返回 true。

如果 CoderResult 实例表示错误条件,length()方法通知您错误输入序列的长度。对于常规的下 溢/上溢条件,没有相关的长度(就是为什么可以共享单实例)。如果您在 CoderResult 的实例上调 用 length(),CoderResult 不表达错误(isError()返回 false)的情况下,它会抛出未检测的 java.lang.UnsupportedOperation-Exception,所以要小心。对于长度实例,输入 CharBuffer 会被放置 在首个错误的字符上。

CoderResult 类同样也包含了三种简便方法,利于自定义编码器和解码器(6.3 节您将学习如何 编写)的开发人员进行开发。CoderResult 构造函数是私有的:您不能直接对它进行实例化或者继 承为自己的。我们已经看到 CoderResult 实例表示上溢和下溢是独立的。对于表示错误的实例,它 们获取的唯一独特的位信息是 length()返回的值。两个工厂方法 malformedForLength()和 unmappableForLength()分别返回 CoderResult 的实例,从 isMalformed()或 isUnmappable()返回 true,并且它们的 length()方法返回您提供的值。这些工厂方法总是根据给出的长度返回相同的 CoderResult 实例。

在某些情况下,相比于沿着 CoderResult 对象导入,抛出一个异常要更合适一些。例如,CharsetEncoder 类的全能 encode()方法,如果它遭遇编码错误则抛出一个异常。throwException()方 法是简便方法,抛出 CharsetCodingException 适当的子类,详见表 6-3.

4)字符集解码器

字符集解码器是编码器的逆转。通过特殊的编码方案把字节编码转化成 16-位 Unicode 字符的 序列。与 CharsetEncoder 类似的, CharsetDecoder 是状态转换引擎。两个都不是线程安全的,因为 调用它们的方法的同时也会改变它们的状态,并且这些状态会被保留下来。

package java.nio.charset;

import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;

public abstract class CharsetDecoder {

    public final Charset charset()

    public final float averageCharsPerByte()

    public final float maxCharsPerByte()

    public boolean isAutoDetecting()

    public boolean isCharsetDetected()

    public Charset detectedCharset()

    public final CharsetDecoder reset()

    public final CharBuffer decode(ByteBuffer in) throws CharacterCodingException

    public final CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)

    public final CoderResult flush(CharBuffer out)

    public CodingErrorAction malformedInputAction()

    public final CharsetDecoder onMalformedInput(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)

    public final String replacement()

    public final CharsetDecoder replaceWith(String newReplacement)
}

像您能看到的,CharsetDecoder 的 API 几乎是 CharsetEncoder 的映像。本节我们将集中在差异 上,继续进行您在 6.2.3 节中已经了解的假设。

前面清单中方法的第一个编组是不言自明的。通过调用 charset()可以获取相关的 Charset 对 象。在这个编码中来自每个字节的字符解码的平均和最大数是分别通过 averageCharsPerByte()和 maxCharsPerByte()返回的。使用这些值可以为 CharBuffer 对象排序,来接收解码的字符。

CharsetDecoder 类有其独特的方法集。在前面的清单中,这些方法不得不处理字符集自检测。 首个方法,isAutoDetecting(),返回表示这个解码器是否能够自检测编码字节序列使用的编码方法的 布尔值。

如果 isAutoDetecting()返回 true,那么前面清单后面的两个方法是有意义的。如果解码器能够 从输入字节序列中读取足够的字节来确定使用的编码类型,那么 isCharsetDetected()方法将返回 true。只有当解码程序已经开始时这个方法才有作用(因为它必须读取一些字节并进行检测)。 调用 reset()之后,它将总是返回 false。这个方法可选并且只对自检测字符集有意义。默认实现总 是抛出 java.lang.UnsupportedOperationException。

如果字符集被检测(用 isCharsetDetected()返回 true 表示),那么通过调用 detectedCharset() 可以获取表示那个字符集的 Charset 对象。除非您知道字符集已经被探测,否则不应该调用这个方 法。如果解码器还没有读取足够的输入,不能确定用编码表达的字符集,将抛出 java.lang.IllegalStateException。detectedCharset()方法同样的也是可选,并且如果字符集没有被自探 测,将抛出同一个 java.lang.UnsupportedOperationException。适当的使用 isAutoDetecting()和 isCharsetDetected(),您应该不会有什么问题。

现在,让我们转到实际完成解码的方法上:

package java.nio.charset;

import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;

public abstract class CharsetDecoder {

    // This is a partial API listing
    public final CharsetDecoder reset()

    public final CharBuffer decode(ByteBuffer in) throws CharacterCodingException

    public final CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)

    public final CoderResult flush(CharBuffer out)
}

 

解码处理和编码类似,包含相同的基本步骤:

  1. 复位解码器,通过调用 reset(),把解码器放在一个已知的状态准备用来接收输入。

  2. 把 endOfInput 设置成 false 不调用或多次调用 decode(),供给字节到解码引擎中。随着解码的 进行,字符将被添加到给定的 CharBuffer 中。

  3. 把 endOfInput 设置成 true 调用一次 decode(),通知解码器已经提供了所有的输入。

  4. 调用 flush(),确保所有的解码字符都已经发送给输出。

 

这在本质上和编码相同(详见 6.2.3 节)。decode()方法同样的也返回 CoderResult 对象,表示 发生的情况。这些结果对象的意义和 CharsetEncoder.encode()返回的意义相同。

当返回下溢或上溢指示时,输入和输出缓冲区应该以跟编码相同的方式进行管理。

现在,处理错误的方法:

package java.nio.charset;

import java.nio.charset.CodingErrorAction;

public abstract class CharsetDecoder {

    // This is a partial API listing
    public CodingErrorAction malformedInputAction()

    public final CharsetDecoder onMalformedInput(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)

    public final String replacement()

    public final CharsetDecoder replaceWith(String newReplacement)
}

API 方法处理在 String 上运行的替换序列,而不是在字节阵列上。解码时,字节序列被转化成 字符序列,所以解码操作的替换序列被指定为 String,String 包含要在错误条件上输出 CharBuffer 中要插入的字符。注意没有用来检测替换序列的 isLegalReplacement()方法。您构造的任何字符串都 是合法的替换序列,除非它不再是 maxCharsPerByte()返回的值。用字符串调用 replaceWith(),太长 将导致 java.lang.IllegalArgumentException。

本节是有意地简洁扼要。详细信息,参考 6.2.3 节。

示例 6-2 说明了如何对表示字符集编码的字节流进行编码。

示例 6-2. 字符集解码

package org.example;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.*;

/**
 * Test charset decoding.
 *
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class CharsetDecode {

    /**
     * Test charset decoding in the general case, detecting and handling
     * buffer under/overflow and flushing the decoder state at end of
     * input.
     * This code reads from stdin and decodes the ASCII-encoded byte
     * stream to chars. The decoded chars are written to stdout. This
     * is effectively a 'cat' for input ascii files, but another charset
     * encoding could be used by simply specifying it on the command line.
     */
    public static void main(String[] argv) throws IOException {
        // Default charset is standard ASCII
        String charsetName = "ISO-8859-1";
        // Charset name can be specified on the command line
        if (argv.length > 0) {
            charsetName = argv[0];
        }
        // Wrap a Channel around stdin, wrap a channel around stdout,
        // find the named Charset and pass them to the decode method.
        // If the named charset is not valid, an exception of type
        // UnsupportedCharsetException will be thrown.
        decodeChannel(Channels.newChannel(System.in), new OutputStreamWriter(System.out), Charset.forName(charsetName));
    }

    /**
     * General purpose static method which reads bytes from a Channel,
     * decodes them according
     *
     * @param source  A ReadableByteChannel object which will be read to
     *                EOF as a source of encoded bytes.
     * @param writer  A Writer object to which decoded chars will be
     *                written.
     * @param charset A Charset object, whose CharsetDecoder will be used
     *                to do the character set decoding.
     */
    public static void decodeChannel(ReadableByteChannel source, Writer writer, Charset charset) throws UnsupportedCharsetException, IOException {
        // Get a decoder instance from the Charset
        CharsetDecoder decoder = charset.newDecoder();
        // Tell decoder to replace bad chars with default mark
        decoder.onMalformedInput(CodingErrorAction.REPLACE);
        decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
        // Allocate radically different input and output buffer sizes
        // for testing purposes
        ByteBuffer bb = ByteBuffer.allocateDirect(16 * 1024);
        CharBuffer cb = CharBuffer.allocate(57);
        // Buffer starts empty; indicate input is needed
        CoderResult result = CoderResult.UNDERFLOW;
        boolean eof = false;
        while (!eof) {
            // Input buffer underflow; decoder wants more input
            if (result == CoderResult.UNDERFLOW) {
                // decoder consumed all input, prepare to refill
                bb.clear();
                // Fill the input buffer; watch for EOF
                eof = (source.read(bb) == -1);
                // Prepare the buffer for reading by decoder
                bb.flip();
            }
            // Decode input bytes to output chars; pass EOF flag
            result = decoder.decode(bb, cb, eof);
            // If output buffer is full, drain output
            if (result == CoderResult.OVERFLOW) {
                drainCharBuf(cb, writer);
            }
        }
        // Flush any remaining state from the decoder, being careful
        // to detect output buffer overflow(s)
        while (decoder.flush(cb) == CoderResult.OVERFLOW) {
            drainCharBuf(cb, writer);
        }
        // Drain any chars remaining in the output buffer
        drainCharBuf(cb, writer);
        // Close the channel; push out any buffered data to stdout
        source.close();
        writer.flush();
    }

    /**
     * Helper method to drain the char buffer and write its content to
     * the given Writer object. Upon return, the buffer is empty and
     * ready to be refilled.
     *
     * @param cb     A CharBuffer containing chars to be written.
     * @param writer A Writer object to consume the chars in cb.
     */
    static void drainCharBuf(CharBuffer cb, Writer writer) throws IOException {
        cb.flip(); // Prepare buffer for draining
        // This writes the chars contained in the CharBuffer but
        // doesn't actually modify the state of the buffer.
        // If the char buffer was being drained by calls to get(),
        // a loop might be needed here.
        if (cb.hasRemaining()) {
            writer.write(cb.toString());
        }
        cb.clear(); // Prepare buffer to be filled again
    }
}

字符集和相关的编码器与解码器差不多就是这些内容了。下一节将介绍 Charset SPI。

3.字符集服务器提供者接口

Charset SPI 为开发人员提供了一种机制,可以在运行中的 JAM 环境中添加新的 Charset 实 现。如果您需要创建自己的字符集,或者导入您正在使用而 JVM 平台没有提供的字符集,那么您 需要字符集 SPI。

可插拔的 SPI 结构是在许多不同的内容中贯穿于 Java 环境使用的。在 1.4JDK 中有八个包,一 个叫 spi 而剩下的有其它的名称。可插拔是一个功能强大的设计技术,是在 Java 的可移植性和适 应性上建立的基石之一。

字符集是由 IANA 正式定义,而标准的字符集是在那里注册的。从 1.4 起在 Java 中处理字符集 是以 IANA 发布的惯例和标准为基础。IANA 不仅仅注册名称,而且对这些名称(RFC2278)的结构 和内容也有规定。如果您创建新的 Charset 实现,您应该遵循字符集名称的惯例。Charset 类强迫实 现相同的规则。例如,字符集的名称必须由表 6-4 中列出的字符集合组成,并且首字符必须是字母 或阿拉伯数字。

在浏览 API 之前,需要解释一下 Charset SPI 如何工作。java.nio.charset.spi 包仅包含一个抽取 类,CharsetProvider。这个类的具体实现供给与它们提供过的 Charset 对象相关的信息。为了定义 自定义字符集,您首先必须从 java.nio.charset package 中创建 Charset, CharsetEncoder, 以及 CharsetDecoder 的具体实现。然后您创建 CharsetProvider 的自定义子类,它将把那些类提供 给 JVM。

6.3.2 节中列出了自定义字符集和提供方的完整取样实现。

1)创建自定义字符集

在了解 java.nio.charset.spi 包中的唯一的类之前,让我们在 java.nio.charset 中 再花点时间并讨论一下实现自定义字符集需要什么。在您能使 Charset 对象在运行的 JVM 中有效之 前,您需要创建它。让我们再看一下 Charset API,添加构造函数并为抽取方法添加注释:

package java.nio.charset;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.util.Locale;
import java.util.Set;
import java.util.SortedMap;

public abstract class Charset implements Comparable<Charset> {

    protected Charset(String canonicalName, String[] aliases)

    public static SortedMap availableCharsets()

    public static boolean isSupported(String charsetName)

    public static Charset forName(String charsetName)

    public final String name()

    public final Set aliases()

    public String displayName()

    public String displayName(Locale locale)

    public final boolean isRegistered()

    public boolean canEncode()

    public abstract CharsetEncoder newEncoder();

    public final ByteBuffer encode(CharBuffer cb)

    public final ByteBuffer encode(String str)

    public abstract CharsetDecoder newDecoder();

    public final CharBuffer decode(ByteBuffer bb)

    public abstract boolean contains(Charset cs);

    public final boolean equals(Object ob)

    public final int compareTo(Object ob)

    public final int hashCode()

    public final String toString()
}

 

您至少要做的是创建 java.nio.charset.Charset 的子类、提供三个抽取方法的具体实现以及一个 构造函数。Charset 类没有默认的,无参数的构造函数。这表示您的自定义字符集类必须有一个构 造函数,即使它不接受参数。这是因为您必须在实例化时调用 Charset 的构造函数(通过在您的构 造函数的开端调用 super()),从而通过您的字符集规范名称和别名供给它。这样做可以让 Charset 类中的方法帮您处理和名称相关的事情,所以是件好事。

通过获取您自定义的编码器和解码器,三个抽取方法中的两个可以被工厂化。同样的您也要实 现布尔方法 contains(),但是您可以用总是返回 false 提出它,false 表示您不知道您的字符集是 否包含给出的字符集。大多数情况下,所有其他的 Charset 方法都有可以使用的默认实现。如果您 的字符集有特殊需求,相应的覆盖默认方法。

同样地,您需要提供 CharsetEncoder 和 CharsetDecoder 的具体实现。回想一下,字符集是编码 的字符和编码/解码方案的集合。如我们之前所看到的,编码和解码在 API 水平上几乎是对称的。 这里给出了关于实现编码器所需要的东西的简短讨论:一样适用于建立解码器。这是 CharsetEncoder 类的清单,用它的构造函数和受保护的以及添加的抽取方法:

package java.nio.charset;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

/**
 * @author wuxingye
 * @date 2020/4/6
 */
public abstract class CharsetEncoder {

    protected CharsetEncoder(Charset cs, float averageBytesPerChar, float maxBytesPerChar)

    protected CharsetEncoder(Charset cs, float averageBytesPerChar, float maxBytesPerChar, byte[] replacement)

    public final Charset charset()

    public final float averageBytesPerChar()

    public final float maxBytesPerChar()

    public final CharsetEncoder reset()

    protected void implReset()

    public final ByteBuffer encode(CharBuffer in) throws CharacterCodingException

    public final CoderResult encode(CharBuffer in, ByteBuffer out, boolean endOfInput)

    public final CoderResult flush(ByteBuffer out)

    protected CoderResult implFlush(ByteBuffer out)

    public boolean canEncode(char c)

    public boolean canEncode(CharSequence cs)

    public CodingErrorAction malformedInputAction()

    public final CharsetEncoder onMalformedInput(CodingErrorAction newAction)

    protected void implOnMalformedInput(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final CharsetEncoder onUnmappableCharacter(CodingErrorAction newAction)

    protected void implOnUnmappableCharacter(CodingErrorAction newAction)

    public final byte[] replacement()

    public boolean isLegalReplacement(byte[] repl)

    public final CharsetEncoder replaceWith(byte[] newReplacement)

    protected void implReplaceWith(byte[] newReplacement)

    protected abstract CoderResult encodeLoop(CharBuffer in, ByteBuffer out);
}

与 Charset 类似的, CharsetEncoder 没有默认的构造函数,所以您需要在具体类构造函数中调用 super(),提供需要的参数。

首先看一下最后一个方法。为了供给您自己的 CharsetEncoder 实现,您至少要提供具体 encodeLoop()方法。对于简单的编码运算法则,其他方法的默认实现应该可以正常进行。注意 encodeLoop()采用和 encode()的参数类似的参数,不包括布尔标志。encode()方法代表到 encodeLoop()的实际编码,它仅需要关注来自 CharBuffer 参数消耗的字符,并且输出编码的字节到 提供的 ByteBuffer 上。

主要的 encode()方法负责存储调用和处理编码错误上的状态。与 encode()类似的,encodeLoop() 方法返回 CoderResult 对象,表示在处理缓冲区时发生的情况。如果您的 encodeLoop()填充了输出 ByteByffer,它应返回 CoderResult.OVERFLOW。如果输入 CharBuffer 耗尽了,应返回 CoderResult.UNDERFLOW。如果您的编码器需要比输入缓冲区中多的输出才能做出编码决定, 您可以通过返回 UNDERFLOW 提前实现直到 CharBuffer 中有充足的输入继续。

上面所列剩下的受保护方法—以 impl 开始—是状态改变回调,当编码器的状态发生改变时通 知实现(您的代码)。所有这些方法的默认实现是存根,不起作用。例如,如果在您的编码器中还 有额外的状态,当编码器重置时您可能要知道。您不能覆盖 reset()方法本身,因为它声明为最后。 当在 CharsetEncoder 上调用 reset()时为您提供 implReset()方法,让您以一种清晰的方式了解发生的 情况。其他 impl 类针对感兴趣的其他事件扮演相同的角色。

作为参考,这是针对 CharsetDecoder 列出的相等的 API:

package java.nio.charset;

import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;

/**
 * @author wuxingye
 * @date 2020/4/6
 */
public abstract class CharsetDecoder {

    protected CharsetDecoder(Charset cs, float averageCharsPerByte, float maxCharsPerByte)

    public final Charset charset()

    public final float averageCharsPerByte()

    public final float maxCharsPerByte()

    public boolean isAutoDetecting()

    public boolean isCharsetDetected()

    public Charset detectedCharset()

    public final CharsetDecoder reset()

    protected void implReset()

    public final CharBuffer decode(ByteBuffer in) throws CharacterCodingException

    public final CoderResult decode(ByteBuffer in, CharBuffer out, boolean endOfInput)

    public final CoderResult flush(CharBuffer out)

    protected CoderResult implFlush(CharBuffer out)

    public CodingErrorAction malformedInputAction()

    public final CharsetDecoder onMalformedInput(CodingErrorAction newAction)

    protected void implOnMalformedInput(CodingErrorAction newAction)

    public CodingErrorAction unmappableCharacterAction()

    public final CharsetDecoder onUnmappableCharacter(CodingErrorAction newAction)

    protected void implOnUnmappableCharacter(CodingErrorAction newAction)

    public final String replacement()

    public final CharsetDecoder replaceWith(String newReplacement)

    protected void implReplaceWith(String newReplacement)

    protected abstract CoderResult decodeLoop(ByteBuffer in, CharBuffer out);
}

现在,我们已经看到了如何实现自定义字符集,包括相关的编码器和解码器,让我们看一下如 何把它们连接到 JVM 中,这样可以利用它们运行代码。

2)供给你的自定义字符集

为了给 JVM 运行时环境提供您自己的 Charset 实现,您必须在 java.nio.charsets.spi 中创建 CharsetProvider 类的具体子类,每个都带有一个无参数构造函数。无参数构造函数很 重要,因为您的 CharsetProvider 类将要通过读取配置文件的全部合格名称进行定位。之后这个类 名称字符串将被导入到 Class.newInstance()来实例化您的提供方,它仅通过无参数构造函数起作 用。

JVM 读取的配置文件定位字符集提供方,被命名为 java.nio.charset.spi.CharsetProvider。它在 JVM 类路径中位于源目录(META-INF/services)中。每一个 Java Archive(Java 档案文件) (JAR)都有一个 META-INF 目录,它可以包含在那个 JAR 中的类和资源的信息。一个名为 META-INF 的目录也可以在 JVM 类路径中放置在常规目录的顶端。

META-INF/services 目录中的每个文件都有一个完全合格的服务提供方类的名称。每个文件的 内容都是完全合格类名称的清单,名称是那个类(所以文件内的每个类的命名都必须是用文件名称 表达的类的 instanceof)的具体实现。JAR 的详细信息请查阅 http://java.sun.com/j2se/1.4/docs/guide/jar/jar.html

当用类加载器优先检查类路径组件时(JAR 或目录),如果存在 META-INF/services 目录,那 么它包含的每个文件都将被处理。每个都被读取,并且列出的所有类都被实例化且注册为通过文件 名称鉴别的类的提供方。在名为 java.nio.charset.spi.CharsetProvider 的文件中放置您的 CharsetProvider 的完全合格名称,您就是正在把它注册为字符集的提供方。

配置文件的格式是一个简单的完全合格的类名称的清单,每行一个。注释字符是#(#, \u0023)。文件必须在 UTF-8 内编码(标准文本文件)。这个服务清单内命名的类不用放在同一 个 JAR 上,但是类必须对同一个背景的类加载器可见(也就是,处于同一个类路径)。如果同一 个 CharsetProvider 类在不止一个服务文件上命名,或者在同一个文件上多次命名,那么它只将作为服务提供方被添加一次。

这个机制易于安装新的 CharsetProvider 和它提供的 Charset 实现。JAR 包含您的字符集实现, 以及服务文件命名,仅需要处于 JVM 的类路径中。您也可以在您的 JVM 中把它安装成一个扩展, 在您的操作系统(大部分情况中是 jre/lib/ext)定义的扩展目录中放置 JAR 即可。然后您的自定义 字符集在每次 JVM 运行时就可用了。

没有指定的 API 机制,把新的字符集添加到 JVM 编程中。独立的 JVM 实现可以提供 API,但 是 JDK1.4 没有提供这样的方法。

现在我们知道如何使用 CharsetProvider 类添加字符集,让我们看一下代码。CharsetProvider 的 API 几乎是没有作用的。提供自定义字符集的实际工作是发生在创建自定义 Charset, CharsetEncoder,以及 CharsetDecoder 类中。CharsetProvider 仅是连接您的字符集和运行时环境的 促进者。

package java.nio.charset.spi;

import java.util.Iterator;

public abstract class CharsetProvider {

    protected CharsetProvider() throws SecurityException

    public abstract Iterator charsets();

    public abstract Charset charsetForName(String charsetName);
}

注意受保护的构造函数。CharsetProvider 不应该用您的代码直接实例化。CharsetProvider 对象 将通过低水平的服务提供方设备被实例化。如果您需要建立提供方可用的字符集,在 CharsetProvider 类中定义默认的构造函数。这可以从外部资源中调用加载的字符集映射信 息,通过算法产生转化的映射等等。同样的,注意 CharsetProvider 的构造函数可以抛出 java.lang.SecurityException。

CharsetProvider 对象的实例化由 SecurityManager 检查(如果安装的情况下)。安全管理必须 允许 java.lang.RuntionPermission(“charsetProvider”),或者不安装新的字符集提供方。字符集 可以包含在安全敏感操作中,例如编码 URL 以及其他数据内容。潜在伤害是显著的。您可能需要 安装一个安全管理,如果在您的应用中有不信任的正在运行的代码,阻止新的字符集。您也需要检 查不信任的 JAR,用来看它们在 META-INF/service 下是否包含服务配置文件,用于安装自定义字符 集提供方(或者任何种类的自定义服务提供方)。

用您提供的 Charset 实现的使用方调用 CharsetProvider 上定义的两个方法。大部分情况是,您 的提供方将被 Charset 类的静态方法调用,用来发现可用字符集相关的信息,但是其他的类也可以 调用这些方法。

调用 Charsets()方法来获取使您的提供方类可用的 Charset 类的清单。它应该返回 java.util.Iterator,列举到给出的 Charset 实例的参数上。Charset.availableCharsets()方法返回的映射 是一个集合体,在每个当前安装的 CharsetProvider 实例上调用 charsets()方法的集合体。

另一种方法,charsetForName(),被调用用来映射字符集名称,规范名称或者别名,到 Charset 对象上。如果您的提供方没有通过要求的名称提供字符集,这个方法应返回 null。

这就是全部了。现在您有创建自己的自定义字符集所有的必要工具以及和它们相关的编码器和解码 器,插入并激活它们,运行 JVM。示例 6-3 中演示了自定义 Charset 和 CharsetProvider 的实现,包 含说明字符集使用的取样代码,编码和解码,以及 Charset SPI。示例 6-3 实现了一个自定义 Charset。

示例 6-3.自定义 Rot13 字符集

package org.example;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;

/**
 * A Charset implementation which performs Rot13 encoding. Rot-13 encoding
 * is a simple text obfuscation algorithm which shifts alphabetical
 * characters by 13 so that 'a' becomes 'n', 'o' becomes 'b', etc. This
 * algorithm was popularized by the Usenet discussion forums many years ago
 * to mask naughty words, hide answers to questions, and so on. The Rot13
 * algorithm is symmetrical, applying it to text that has been scrambled by
 * Rot13 will give you the original unscrambled text.
 * <p>
 * * Applying this Charset encoding to an output stream will cause everything
 * you write to that stream to be Rot13 scrambled as it's written out. And
 * appying it to an input stream causes data read to be Rot13 descrambled
 * as it's read.
 *
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class Rot13Charset extends Charset {

    // the name of the base charset encoding we delegate to
    private static final String BASE_CHARSET_NAME = "UTF-8";
    // Handle to the real charset we'll use for transcoding between
    // characters and bytes. Doing this allows us to apply the Rot13
    // algorithm to multibyte charset encodings. But only the
    // ASCII alpha chars will be rotated, regardless of the base encoding.
    Charset baseCharset;

    /**
     * Constructor for the Rot13 charset. Call the superclass
     * constructor to pass along the name(s) we'll be known by.
     * Then save a reference to the delegate Charset.
     */
    protected Rot13Charset(String canonical, String[] aliases) {
        super(canonical, aliases);
        // Save the base charset we're delegating to
        baseCharset = Charset.forName(BASE_CHARSET_NAME);
    }
    // ----------------------------------------------------------

    /**
     * Called by users of this Charset to obtain an encoder.
     * This implementation instantiates an instance of a private class
     * (defined below) and passes it an encoder from the base Charset.
     */
    public CharsetEncoder newEncoder() {
        return new Rot13Encoder(this, baseCharset.newEncoder());
    }

    /**
     * Called by users of this Charset to obtain a decoder.
     * This implementation instantiates an instance of a private class
     * (defined below) and passes it a decoder from the base Charset.
     */
    public CharsetDecoder newDecoder() {
        return new Rot13Decoder(this, baseCharset.newDecoder());
    }

    /**
     * This method must be implemented by concrete Charsets. We always
     * say no, which is safe.
     */
    public boolean contains(Charset cs) {
        return (false);
    }

    /**
     * Common routine to rotate all the ASCII alpha chars in the given
     * CharBuffer by 13. Note that this code explicitly compares for
     * upper and lower case ASCII chars rather than using the methods
     * Character.isLowerCase and Character.isUpperCase. This is because
     * the rotate-by-13 scheme only works properly for the alphabetic
     * characters of the ASCII charset and those methods can return
     * true for non-ASCII Unicode chars.
     */
    private void rot13(CharBuffer cb) {
        for (int pos = cb.position(); pos < cb.limit(); pos++) {
            char c = cb.get(pos);
            char a = '\u0000';
            // Is it lowercase alpha?
            if ((c >= 'a') && (c <= 'z')) {
                a = 'a';
            }
            // Is it uppercase alpha?
            if ((c >= 'A') && (c <= 'Z')) {
                a = 'A';
            }
            // If either, roll it by 13
            if (a != '\u0000') {
                c = (char) ((((c - a) + 13) % 26) + a);
                cb.put(pos, c);
            }
        }
    }
    // -------------------------------------------------------

    /**
     * The encoder implementation for the Rot13 Charset.
     * This class, and the matching decoder class below, should also
     * override the "impl" methods, such as implOnMalformedInput( ) and
     * make passthrough calls to the baseEncoder object. That is left
     * as an exercise for the hacker.
     */
    private class Rot13Encoder extends CharsetEncoder {

        private CharsetEncoder baseEncoder;

        /**
         * Constructor, call the superclass constructor with the
         * Charset object and the encodings sizes from the
         * delegate encoder.
         */
        Rot13Encoder(Charset cs, CharsetEncoder baseEncoder) {
            super(cs, baseEncoder.averageBytesPerChar(),
                    baseEncoder.maxBytesPerChar());
            this.baseEncoder = baseEncoder;
        }

        /**
         * Implementation of the encoding loop. First, we apply
         * the Rot13 scrambling algorithm to the CharBuffer, then
         * reset the encoder for the base Charset and call it's
         * encode( ) method to do the actual encoding. This may not
         * work properly for non-Latin charsets. The CharBuffer
         * passed in may be read-only or re-used by the caller for
         * other purposes so we duplicate it and apply the Rot13
         * encoding to the copy. We DO want to advance the position
         * of the input buffer to reflect the chars consumed.
         */
        protected CoderResult encodeLoop(CharBuffer cb, ByteBuffer bb) {
            CharBuffer tmpcb = CharBuffer.allocate(cb.remaining());
            while (cb.hasRemaining()) {
                tmpcb.put(cb.get());
            }
            tmpcb.rewind();
            rot13(tmpcb);
            baseEncoder.reset();
            CoderResult cr = baseEncoder.encode(tmpcb, bb, true);
            // If error or output overflow, we need to adjust
            // the position of the input buffer to match what
            // was really consumed from the temp buffer. If
            // underflow (all input consumed), this is a no-op.
            cb.position(cb.position() - tmpcb.remaining());
            return (cr);
        }
    }
    // ------------------------------------------------------

    /**
     * The decoder implementation for the Rot13 Charset.
     */
    private class Rot13Decoder extends CharsetDecoder {

        private CharsetDecoder baseDecoder;

        /**
         * Constructor, call the superclass constructor with the
         * Charset object and pass alon the chars/byte values
         * from the delegate decoder.
         */
        Rot13Decoder(Charset cs, CharsetDecoder baseDecoder) {
            super(cs, baseDecoder.averageCharsPerByte(), baseDecoder.maxCharsPerByte());
            this.baseDecoder = baseDecoder;
        }

        /**
         * Implementation of the decoding loop. First, we reset
         * the decoder for the base charset, then call it to decode
         * the bytes into characters, saving the result code. The
         * CharBuffer is then de-scrambled with the Rot13 algorithm
         * and the result code is returned. This may not
         * work properly for non-Latin charsets.
         */
        protected CoderResult decodeLoop(ByteBuffer bb, CharBuffer cb) {
            baseDecoder.reset();
            CoderResult result = baseDecoder.decode(bb, cb, true);
            rot13(cb);
            return (result);
        }
    }
    // -------------------------------------------------------

    /**
     * Unit test for the Rot13 Charset. This main( ) will open and read
     * an input file if named on the command line, or stdin if no args
     * are provided, and write the contents to stdout via the X-ROT13
     * charset encoding.
     * The "encryption" implemented by the Rot13 algorithm is symmetrical.
     * Feeding in a plain-text file, such as Java source code for example,
     * will output a scrambled version. Feeding the scrambled version
     * back in will yield the original plain-text document.
     */
    public static void main(String[] argv) throws Exception {
        BufferedReader in;
        if (argv.length > 0) {
            // Open the named file
            in = new BufferedReader(new FileReader(argv[0]));
        } else {
            // Wrap a BufferedReader around stdin
            in = new BufferedReader(new InputStreamReader(System.in));
        }
        // Create a PrintStream that uses the Rot13 encoding
        PrintStream out = new PrintStream(System.out, false, "X-ROT13");
        String s = null;
        // Read all input and write it to the output.
        // As the data passes through the PrintStream,
        // it will be Rot13-encoded.
        while ((s = in.readLine()) != null) {
            out.println(s);
        }
        out.flush();
    }
}

为了使用这个 Charset 和它的编码器与解码器,它必须对 Java 运行时环境有效。用CharsetProvider 类完成(示例 6-4)。

package org.example;

import java.nio.charset.Charset;
import java.nio.charset.spi.CharsetProvider;
import java.util.HashSet;
import java.util.Iterator;

/**
 * A CharsetProvider class which makes available the charsets
 * provided by Ronsoft. Currently there is only one, namely the X-ROT13
 * charset. This is not a registered IANA charset, so it's
 * name begins with "X-" to avoid name clashes with offical charsets.
 * <p>
 * * To activate this CharsetProvider, it's necessary to add a file to
 * the classpath of the JVM runtime at the following location:
 * <p>
 * META-INF/services/java.nio.charsets.spi.CharsetProvider
 * That file must contain a line with the fully qualified name of
 * this class on a line by itself:
 * <p>
 * com.ronsoft.books.nio.charset.RonsoftCharsetProvider
 * See the javadoc page for java.nio.charsets.spi.CharsetProvider
 * for full details.
 *
 * @author Ron Hitchens (ron@ronsoft.com)
 */
public class RonsoftCharsetProvider extends CharsetProvider {

    // the name of the charset we provide
    private static final String CHARSET_NAME = "X-ROT13";
    // a handle to the Charset object
    private Charset rot13 = null;

    /**
     * Constructor, instantiate a Charset object and save the reference.
     */
    public RonsoftCharsetProvider() {
        this.rot13 = new Rot13Charset(CHARSET_NAME, new String[0]);
    }

    /**
     * Called by Charset static methods to find a particular named
     * Charset. If it's the name of this charset (we don't have
     * any aliases) then return the Rot13 Charset, else return null.
     */
    public Charset charsetForName(String charsetName) {
        if (charsetName.equalsIgnoreCase(CHARSET_NAME)) {
            return (rot13);
        }
        return (null);
    }

    /**
     * Return an Iterator over the set of Charset objects we provide.
     *
     * @return An Iterator object containing references to all the
     * Charset objects provided by this class.
     */
    public Iterator charsets() {
        HashSet set = new HashSet(1);
        set.add(rot13);
        return (set.iterator());
    }
}

对于通过 JVM 运行时环境看到的这个字符集提供方,名为 META_INF/services/java.nio.charset.spi.CharsetProvider 的文件必须存在于 JARs 之一内或类路径的 目录中。那个文件的内容必须是:

com.ronsoft.books.nio.charset.RonsoftCharsetProvider

在示例 6-1 中的字符集清单中添加 X-ROT13,产生这个额外的输出:

Charset: X-ROT13 
Input: żMańana? 
Encoded:
0: c2 (Ż) 
1: bf (ż) 
2: 5a (Z) 
3: 6e (n) 
4: c3 (Ă) 
5: b1 (±) 
6: 6e (n) 
7: 61 (a) 
8: 6e (n)
9: 3f (?)

字母 a 和 n 是巧合的分离 13 字母,所以它们在这个特殊的字组中出现在转换位置上。注意非ASCII 和非字母字符是如何在 UTF-8 编码中保持不变的。

总结

许多 Java 编程人员永远不会需要处理字符集编码转换问题,而大多数永远不会创建自定义字 符集。但是对于那些需要的人,在 java.nio.charset 和 hava.charset.spi 中的一系列类为字符处理提供 了强大的以及弹性的机制。

本章我们学习了 JDK1.4 中新的字符编码的特性。下面是本章的重点:

Charset(字符集类)

封装编码的字符集编码方案,用来表示与作为字节序列的字符集不同的字符序列。

CharsetEncoder(字符集编码类)

编码引擎,把字符序列转化成字节序列。之后字节序列可以被解码从而重新构造源字符序 列。

CharsetDecoder(字符集解码器类)

解码引擎,把编码的字节序列转化为字符序列。

CharsetProvider SPI(字符集供应商 SPI)

通过服务器供应商机制定位并使 Charset 实现可用,从而在运行时环境中使用。

 

基本上,我们完成了 NIO 的神奇之旅了。检查一下座位的四周,看看有没有落下的个人物 品。非常感谢您的关注,务必要访问我们的网址 http://www.javanio.info/。再见。

摘自JAVA NIO(中文版)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值