1. DOM (Document Object Model)
有时 XML 文件太大,无法一次性加载到内存中。也可能文档结构事先未知或过于复杂。还有些场景只需处理数据的部分片段,而非整个文件,或者需要在程序运行时直接修改文档:删除节点、添加新元素或重构部分结构。
在这些情况下,经典而可靠的工具——DOM 与 SAX——就派上用场了。它们允许你在不依赖预先定义的 Java 类的前提下,直接操作 XML 的内容。
DOM 的工作原理
DOM 是一种在内存中用对象树来表示 XML 文档的方式。每个标签都会变成树中的一个节点(Node),而属性、文本值甚至注释都会成为独立的对象。加载文档后,你可以完全访问其结构:读取、修改、删除、添加元素与属性。
Java 中 DOM 的主要类
- DocumentBuilderFactory — 用于创建解析器的工厂。
- DocumentBuilder — 将 XML 转换为树的解析器。
- Document — 树的根对象。
- Element — XML 元素(标签)。
- NodeList — 节点列表(例如,<items> 内部的所有 <item>)。
示例:使用 DOM 读取 XML 文件
假设我们需要读取如下 XML 文件:
<contacts>
<person id="1">
<name>Ivan</name>
<email>ivan@example.com</email>
</person>
<person id="2">
<name>Maria</name>
<email>maria@example.com</email>
</person>
</contacts>
代码:读取并遍历元素
import javax.xml.parsers.*;
import org.w3c.dom.*;
import java.io.File;
public class DomExample {
public static void main(String[] args) throws Exception {
// 1. 创建工厂和解析器
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
// 2. 将 XML 文件加载到内存
Document doc = builder.parse(new File("contacts.xml"));
// 3. 获取根元素
Element root = doc.getDocumentElement();
System.out.println("根元素:" + root.getTagName());
// 4. 获取所有 <person> 的列表
NodeList persons = root.getElementsByTagName("person");
for (int i = 0; i < persons.getLength(); i++) {
Element person = (Element) persons.item(i);
String id = person.getAttribute("id");
String name = person.getElementsByTagName("name").item(0).getTextContent();
String email = person.getElementsByTagName("email").item(0).getTextContent();
System.out.println("id: " + id + ", name: " + name + ", email: " + email);
}
}
}
这里发生了什么:
- 先创建解析器并加载 XML 文件。
- 获取根元素(contacts)。
- 查找所有 <person> 元素并进行遍历。
- 对每个 <person> 读取 id 属性,以及 <name> 和 <email> 元素。
本例的 DOM 树示意
contacts
├── person (id="1")
│ ├── name ("Ivan")
│ └── email ("ivan@example.com")
└── person (id="2")
├── name ("Maria")
└── email ("maria@example.com")
使用 DOM 修改 XML
DOM 不仅能读取,还能修改 XML。例如,添加一个新联系人:
// 创建新的元素 <person>
Element newPerson = doc.createElement("person");
newPerson.setAttribute("id", "3");
// <name>
Element name = doc.createElement("name");
name.setTextContent("Sergey");
newPerson.appendChild(name);
// <email>
Element email = doc.createElement("email");
email.setTextContent("sergey@example.com");
newPerson.appendChild(email);
// 添加到根元素
root.appendChild(newPerson);
// 将修改保存到文件
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(new DOMSource(doc), new StreamResult(new File("contacts-updated.xml")));
DOM 的主要优缺点
- 优点:适合小文件,易于做任意修改,可以在树中任意方向“游走”。
- 缺点:整个 XML 都在内存中。对于大文件(数百 MB 甚至更大)并不合适,内存很快会耗尽。
2. SAX (Simple API for XML)
SAX 是事件驱动的解析器。它不构建树,而是“从左到右”读取 XML,并触发事件处理器:开始元素、文本内容、结束元素等。你编写自己的处理器,按需响应这些事件。
类比:
如果 DOM 像是把活页手册的所有页面摊在桌面上,SAX 则像是逐页阅读手册,只有遇到需要的页面才做笔记。
Java 中 SAX 的主要类
- SAXParserFactory、SAXParser — 工厂与解析器。
- DefaultHandler — 处理事件的基类。
- 事件处理方法:startElement、characters、endElement、startDocument、endDocument。
示例:使用 SAX 读取 XML 文件
还是同一个文件 contacts.xml。我们只想打印所有人的姓名与 e-mail。
代码:SAX 解析器
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
public class SaxExample {
public static void main(String[] args) throws Exception {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(new File("contacts.xml"), new ContactHandler());
}
}
class ContactHandler extends DefaultHandler {
private String currentElement = "";
private String name = "";
private String email = "";
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
if ("person".equals(qName)) {
String id = attributes.getValue("id");
System.out.println("新联系人,id: " + id);
}
}
@Override
public void characters(char[] ch, int start, int length) {
String text = new String(ch, start, length).trim();
if (text.isEmpty()) return;
if ("name".equals(currentElement)) {
name = text;
} else if ("email".equals(currentElement)) {
email = text;
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("person".equals(qName)) {
System.out.println("姓名: " + name + ", email: " + email);
name = "";
email = "";
}
currentElement = "";
}
}
现在用通俗的话解释一下这里发生了什么。当 SAX 解析器遇到一个起始标签(例如 <person>)时,会调用 startElement 方法。在那里我们立即读取 id 属性并打印出来。当遇到内部文本(例如姓名或 e-mail)时,控制权会进入 characters 方法,我们把文本暂存到临时变量中。而当解析器读到结束标签 </person> 时,会调用 endElement。此时我们已经知道了此人的姓名与 e-mail,可以打印它们,然后清空变量,以便处理下一个联系人。
核心思想是,SAX 不把整个 XML 全部保存在内存里,而是像“流式阅读器”一样,自上而下遍历文件,并对事件作出响应——标签开始、文本、标签结束。这既快又省内存,特别适合大文件。
DOM 与 SAX 的简要对比
| DOM | SAX | |
|---|---|---|
| 风格 | 树(整个结构在内存中) | 事件(“即时”处理) |
| 内存 | 加载整个 XML | 最小内存占用 |
| 修改 | 可读/可改/可增 | 只读(通常) |
| 数据查找 | 在任意位置都易于查找 | 需要自己跟踪“当前所在位置” |
| 文件大小 | 适用于小型和中等大小文件 | 适用于超大文件 |
3. 何时使用 DOM,何时使用 SAX
DOM 很适合以下情况:
- 文件体积较小或中等大小。
- 需要多次访问 XML 的不同部分。
- 需要修改文档结构。
SAX 适用于以下情况:
- 文件非常大,无法整体加载到内存。
- 只需快速“抽取”部分信息(例如查找所有具有特定属性的 <item> 元素)。
- 需要高性能与最低的内存占用。
- 不打算修改 XML,只进行读取。
建议:
在真实项目中常常两者并用:DOM 用于“更贴近人类”的设置与小型配置,SAX 用于处理日志、导出/导入和超大规模数据。
4. 实践任务:为应用编写一个小型解析器
在你的练习应用(例如“联系人簿”)中,出现了这样一个需求:快速统计使用 gmail.com 邮箱的用户数量。
DOM 方案:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("contacts.xml"));
NodeList emails = doc.getElementsByTagName("email");
int count = 0;
for (int i = 0; i < emails.getLength(); i++) {
String email = emails.item(i).getTextContent();
if (email.endsWith("@gmail.com")) {
count++;
}
}
System.out.println("gmail.com 用户数:" + count);
SAX 方案:
class GmailCounterHandler extends DefaultHandler {
private String currentElement = "";
int count = 0;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
currentElement = qName;
}
@Override
public void characters(char[] ch, int start, int length) {
if ("email".equals(currentElement)) {
String email = new String(ch, start, length).trim();
if (email.endsWith("@gmail.com")) {
count++;
}
}
}
@Override
public void endDocument() {
System.out.println("gmail.com 用户数:" + count);
}
}
5. 使用 DOM 和 SAX 的特点与细节
- DOM 在 XML 文件非常大时可能“吃光”内存。如果你看到 OutOfMemoryError,很可能该切换到 SAX 了.
- SAX 需要足够细心:要自己跟踪当前处于哪个元素,并谨慎聚合所需数据。在深度嵌套的结构中,有时需要使用栈或额外变量避免混乱。
- 在 SAX 中,characters 对同一段文本可能被调用多次(尤其当文本很长或包含特殊字符时)。最好用 StringBuilder 累积文本。
- DOM 非常适合搜索、导航与结构修改,但不适合流式处理。
- 如果不确定该选哪种方式——为简单起见先用 DOM;一旦内存“吃不消”,再改为 SAX。
6. 使用 DOM 和 SAX 的常见错误
错误 1:在 SAX 中错误处理空格和换行。
characters 方法可能会返回文本碎片,包括元素之间的空格和换行。如果不筛掉 .trim().isEmpty(),就会产生大量“空”调用或拼接出错误文本。
错误 2:试图用 SAX 修改 XML。
SAX 仅用于读取!如果需要修改结构——请使用 DOM。
错误 3:破坏 SAX 的事件顺序。
如果不在 endElement 中重置变量,数据可能会在元素之间“串味”。
错误 4:对超大文件使用 DOM。
结果要么是 OutOfMemoryError,要么是非常慢的性能。
错误 5:在 DOM 中错误的类型转换。
在 DOM 中一切都是 Node,但操作属性和子元素需要转成 Element。转换错误可能导致 ClassCastException。
GO TO FULL VERSION