1. 从一次模糊查找翻车说起Cursor 到底指向哪里Android 里用 SQLite 做本地搜索Cursor是绕不开的东西。你可以把它理解成一个「结果集的游标」——它不直接给你数据而是像一根指针指向查询返回的某一行你通过moveToFirst()、moveToNext()移动它再用getString()、getInt()把当前行的列值取出来。问题就出在这个「指针」的初始位置上它默认停在第一行之前也就是下标 -1 的位置而不是 0。我见过太多新手包括当年的自己写出这样的代码rawQuery拿到 Cursor 后直接getString()结果抛出android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1。报错信息其实已经把答案写脸上了——请求了下标 -1但结果集大小是 1。也就是说数据明明查到了只是游标没挪到有效行上。模糊查找场景更容易踩坑因为搜索框里的内容千变万化用户可能输入空字符串、可能什么都不输、也可能传进来一个null。这时候LIKE ?的拼接参数%s%就会产生完全不同的 SQL 语义。再叠加 Java 里String s null和String s 的区别查询结果可能从「返回全部」变成「返回空」甚至直接崩掉。这篇就围绕 Android SQLite Cursor 模糊查找这条线把空对象与空值的差异、可复制的查询代码、以及验证步骤一次讲透。2. 前置准备TaoToken 接入与工程环境确认在动手改代码之前先把两件事准备好一是你的 Android 工程能正常跑 SQLite二是如果你打算用大模型辅助排查这类 Cursor 报错或生成 SQL可以先把 TaoToken 的接入配置好。TaoToken 是一个面向开发者的模型调用入口适合用来做代码解释、报错分析和 SQL 片段生成尤其在你对LIKE通配符语义拿不准的时候让它帮你推演一下参数拼接结果会省不少时间。接入本身不复杂核心就是拿到 API Key然后按文档把请求发出去。你可以先到 TaoToken API Keys 生成一个密钥再对照 接入文档 把 base URL 和鉴权头配好。API 地址是https://taotoken.net/api注意这个地址不带任何查询参数直接作为请求前缀使用即可。工程侧你需要确认SQLiteOpenHelper已经建好表表里至少有一个文本列比如detail用来做模糊匹配minSdkVersion不用特别调整rawQuery从很早就支持另外建议在build.gradle里确认没有引入会干扰 Cursor 的第三方 ORM避免排查时被中间层掩盖真实报错。提示如果你只是想在本地验证 SQL 语义不一定非要连真机用 Android Studio 的 Database Inspector 也能直接看查询结果但 Cursor 的游标行为还是得在代码里跑才能复现。3. 可复制配置模糊查找 SQL 与 Cursor 遍历代码先看最核心的模糊查找写法。假设表名是notecontent要按detail列做包含匹配标准写法是用LIKE加通配符参数通过selectionArgs传入不要自己拼字符串String s content; Cursor c db.rawQuery( SELECT * FROM notecontent WHERE detail LIKE ?, new String[]{% s %} );这里?是占位符selectionArgs里的%s%会被安全绑定避免 SQL 注入。注意LIKE在 SQLite 里默认对 ASCII 大小写不敏感但对中文没有大小写概念所以中文模糊查找直接用就行。接下来是 Cursor 遍历的正确姿势。关键点是任何取值之前先判断moveToFirst()是否成功。如果结果集为空moveToFirst()返回false此时绝不能取值Cursor cursor null; try { cursor db.rawQuery( SELECT * FROM notecontent WHERE detail LIKE ?, new String[]{% s %} ); if (cursor ! null cursor.moveToFirst()) { do { int id cursor.getInt(cursor.getColumnIndexOrThrow(_id)); String detail cursor.getString(cursor.getColumnIndexOrThrow(detail)); // 处理每一行数据 } while (cursor.moveToNext()); } } finally { if (cursor ! null) { cursor.close(); } }用getColumnIndexOrThrow而不是getColumnIndex是为了在列名写错时立刻抛异常而不是返回 -1 导致后续取值错位。do-while配合moveToNext()能保证第一行也被处理到这是遍历多行结果的标准模式。现在重点来了String s的两种「空」。当s null时% s %的结果是字符串%null%因为 Java 字符串拼接会把null转成字面量null。这意味着查询会去找包含「null」这四个字母的记录而不是返回全部。当s 时拼接结果是%%LIKE %%在 SQLite 里匹配任意字符串等价于返回全部记录。两者行为完全不同这就是空对象与空值在模糊查找里最直接的差异。如果你希望「搜索框为空时返回全部」正确做法是显式判断String keyword (s null) ? : s.trim(); Cursor c db.rawQuery( SELECT * FROM notecontent WHERE detail LIKE ?, new String[]{% keyword %} );这样null被归一化成空字符串LIKE %%返回全部符合搜索框的常见预期。如果你希望「空输入时不返回任何结果」那就得改成WHERE detail LIKE ? AND ? ! 之类的条件或者直接在 Java 层拦截。4. 验证请求与成功结果三种输入的实际表现光看代码不够得跑一遍看结果。假设表notecontent里有三条记录detail分别是android content、sqlite note、null value test。下面用三种输入分别验证。第一种s content。拼接后参数是%content%查询返回第一条android content。moveToFirst()返回true遍历一次getString拿到正确值moveToNext()返回false结束。这是最正常的路径。第二种s 。拼接后参数是%%LIKE %%匹配所有非 NULL 的文本返回全部三条。moveToFirst()为truedo-while循环三次依次取出三条记录。如果你在搜索框清空时看到列表刷新成全部数据就是这个行为。第三种s null。如果不做归一化拼接后参数是%null%查询只返回第三条null value test因为只有它包含「null」这个子串。这就是很多人遇到的「明明没输入内容却只搜出一条奇怪记录」的根因。如果你做了s null ? : s的归一化结果就和第二种一致返回全部。验证时可以在do-while里打日志Log.d(CursorTest, id id , detail detail);然后分别用三种输入跑一遍对比 Logcat 输出。实测下来归一化处理能消除null带来的语义歧义让搜索行为可预测。注意如果detail列本身存了 NULL 值不是字符串 null而是数据库 NULLLIKE %%不会匹配到它因为 SQL 里 NULL 参与任何比较都返回 UNKNOWN。这时候需要用WHERE detail LIKE ? OR detail IS NULL来兜底。5. 本篇常见错排查CursorIndexOutOfBounds 与空值陷阱第一个高频错误就是开头提到的CursorIndexOutOfBoundsException: Index -1 requested。原因只有一个取值前没调moveToFirst()或者调了但没判断返回值。修复方式就是前面代码里的if (cursor ! null cursor.moveToFirst())。注意moveToFirst()返回false时不要进循环否则getColumnIndex之后取值依然会越界。第二个坑是getColumnIndex返回 -1。当你把列名拼错比如写成details而实际列是detailgetColumnIndex返回 -1getString(-1)就会抛CursorIndexOutOfBoundsException。换成getColumnIndexOrThrow能在列名错误时直接告诉你哪一列不存在排查更快。第三个坑是null与混用导致查询结果不符合预期。除了前面说的拼接问题还有一种情况从Intent或EditText拿到的搜索词可能是null你直接传给rawQuery的selectionArgs虽然不会崩但语义已经偏了。统一在入口处做keyword (input null) ? : input.trim()能省掉大量调试时间。第四个坑是忘记close()。Cursor 不关会导致内存泄漏尤其在频繁查询的场景。用try-finally包住或者用 try-with-resourcesCursor 实现了CloseableAPI 16 可用try (Cursor cursor db.rawQuery(sql, args)) { if (cursor.moveToFirst()) { // 遍历 } }第五个坑是LIKE通配符没转义。如果用户输入的搜索词里本身包含%或_它们会被当成通配符导致匹配范围扩大。比如搜50%会匹配到50开头的任意内容。需要转义时可以用ESCAPE子句String escaped s.replace(%, \\%).replace(_, \\_); db.rawQuery(SELECT * FROM notecontent WHERE detail LIKE ? ESCAPE \\, new String[]{% escaped %});这几个坑基本覆盖了 Cursor 模糊查找里 90% 的异常。遇到报错时先看异常类型Index -1查游标位置Index -1且列名相关查getColumnIndex结果不对查null与的拼接语义。6. 语义一致 CTA按场景选对入口如果你现在卡在某个具体报错上比如CursorIndexOutOfBoundsException反复出现或者不确定LIKE参数拼接后到底生成了什么 SQL最直接的办法是把报错和代码片段丢给模型对话让它帮你逐行推演。可以走 模型对话 入口把 Cursor 相关代码和 Logcat 一起贴进去通常几轮就能定位到是游标位置问题还是空值语义问题。如果你是在做长期的 Android 本地存储模块或者要写一套可复用的 DAO 层涉及大量 SQL 生成和边界条件处理那更适合用 Coding Plan 来做持续性的代码辅助把模糊查找、空值归一化、游标遍历这些模式沉淀成模板。至于接入配置本身API Key 在 控制台 里管理需要新增或轮换密钥时去 API Keys 页面操作请求格式和鉴权细节以 接入文档 为准。把这几处按你的实际场景选对排查效率会明显不一样。