
基于栈的JSON结构验证原理
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,其核心特点是结构化和易读性。JSON结构主要由对象({})和数组([])构成,键值对中的字符串则使用双引号("")包裹。验证JSON字符串的结构有效性,通常需要检查这些括号和引号是否正确配对和嵌套。栈(Stack)数据结构非常适合解决这类配对问题,因为它遵循“后进先出”(LIFO)原则,可以方便地匹配开闭符号。
基本思路是:
- 遇到开符号({, [),将其压入栈中。
- 遇到闭符号(}, ]),检查栈顶是否为对应的开符号,如果是则弹出栈顶元素;否则,结构无效。
- 遇到双引号("),需要特殊处理,因为引号内部的字符(包括 {, }, [, ])不应被视为结构符号。
- 遍历结束后,如果栈为空,则结构可能有效。
初步尝试与常见问题分析
以下是一个基于栈的JSON结构验证的初步尝试代码:
import java.util.Stack;
public class JsonValidator {
public static boolean isValidJSON(String jsonString) {
Stack stack = new Stack<>();
for (char c : jsonString.toCharArray()) {
switch (c) {
case '{':
stack.push(c);
break;
case '}':
if (stack.isEmpty()) {
return false;
}
Character last1 = stack.pop();
if (last1 != '{') {
return false;
}
break;
case '[':
stack.push(c);
break;
case ']':
if (stack.isEmpty()) {
return false;
}
Character last2 = stack.pop();
if (last2 != '[') {
return false;
}
break;
case '\"':
// 这里的逻辑存在多处问题
if (stack.isEmpty()) {
// 错误:如果字符串以双引号开始,栈是空的,会直接返回false
return false;
}
Character last3 = stack.peek();
if (last3 == '\"') {
stack.pop(); // 期望是匹配一个开引号
} else {
stack.push(c); // 期望是压入一个开引号
}
// 核心问题:无论上面if-else分支如何,这里总是会再次压入双引号
stack.push(c);
break;
}
}
return stack.isEmpty();
}
} 这段代码存在以下几个关键问题:
立即学习“Java免费学习笔记(深入)”;
-
双引号处理逻辑不当:
- 在 case '\"' 中,if (stack.isEmpty()) { return false; } 这一判断是错误的。一个有效的JSON字符串完全可以以双引号开始(例如 "hello"),此时栈是空的,不应直接判定为无效。
- 在处理双引号时,stack.push(c); 语句被执行了两次(一次在 else 分支中,一次在 case '\"' 块的末尾)。这会导致每次遇到双引号都会向栈中压入至少一个(甚至两个)引号,使得栈永远无法清空,从而错误地判定字符串无效。
- 将双引号直接压入主栈来匹配,没有区分字符串内部和外部的上下文,导致结构字符(如{, })在字符串内部时也会被错误处理。
未处理字符串内部的结构字符: 在JSON中, {, }, [, ] 等字符如果出现在双引号内部,应被视为普通字符串内容,不应参与结构匹配。例如,"{\"key\":\"value\"}" 是一个有效的JSON字符串,其中的 { 和 } 是字符串的一部分,不应影响栈的平衡。上述代码没有这种上下文感知能力。
未处理转义字符: JSON字符串中支持转义字符,例如 \" 表示一个字面量的双引号。原始代码没有识别和跳过转义序列,可能导致 \" 被错误地识别为字符串的结束引号,从而破坏逻辑。
改进的JSON结构验证实现
为了解决上述问题,我们需要引入一个状态变量来判断当前是否处于字符串内部,并正确处理转义字符。
import java.util.Stack;
public class JsonValidatorImproved {
/**
* 验证JSON字符串的结构有效性(仅限括号和引号的平衡与嵌套)
* 不进行完整的JSON语法验证(如键值对格式、数据类型有效性等)
*
* @param jsonString 待验证的JSON字符串
* @return 如果结构平衡且嵌套正确,则返回true;否则返回false。
*/
public static boolean isValidJSONStructure(String jsonString) {
Stack stack = new Stack<>();
boolean inString = false; // 标记当前是否在双引号字符串内部
for (int i = 0; i < jsonString.length(); i++) {
char c = jsonString.charAt(i);
// 如果当前在字符串内部
if (inString) {
// 处理转义字符:如果遇到反斜杠,跳过下一个字符
if (c == '\\') {
i++; // 跳过下一个字符,因为它是转义序列的一部分
if (i >= jsonString.length()) { // 防止字符串在转义符后结束,导致越界
return false; // 非法转义序列
}
continue; // 继续处理下一个字符
}
// 遇到未转义的双引号,表示字符串结束
if (c == '\"') {
inString = false;
}
// 字符串内部的其他字符(包括 { } [ ])不影响结构匹配,直接跳过
continue;
}
// 不在字符串内部时,处理结构字符
switch (c) {
case '{':
case '[':
stack.push(c); // 遇到开符号,压入栈
break;
case '}':
if (stack.isEmpty() || stack.pop() != '{') {
return false; // 栈为空或栈顶不匹配,结构无效
}
break;
case ']':
if (stack.isEmpty() || stack.pop() != '[') {
return false; // 栈为空或栈顶不匹配,结构无效
}
break;
case '\"':
inString = true; // 遇到双引号,进入字符串模式
break;
// 忽略其他字符,如逗号、冒号、空格、数字、布尔值、null等。
// 这些字符在结构验证中不影响括号平衡,但完整的JSON验证需要检查它们。
case ' ':
case '\t':
case '\n':
case '\r':
case ',':
case ':':
break; // 忽略空白符、逗号和冒号
default:
// 对于不在字符串内部的非结构字符,如果不是合法JSON值的一部分,
// 这里可以根据需求返回false,但对于纯粹的结构平衡验证,可以忽略。
// 为了简化,我们只关注括号和引号的平衡。
// 实际的JSON解析器会在这里进行更严格的字符检查。
// 例如,如果是非数字、非布尔、非null的裸字符,可能是语法错误。
// 为了本教程的目的,我们假设这些字符要么是合法值的一部分,要么是外部的非结构字符。
break;
}
}
// 最终检查:栈必须为空,且不能停留在字符串内部
return stack.isEmpty() && !inString;
}
public static void main(String[] args)










