“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
Gson那个从不吭声的静默Bug(面试必问)
先来一个让人崩溃的场景
假设你在写一个Pokédex应用,定义了一个数据类:
data class PokemonStat(
@SerializedName("base_stat")
val baseStat: Int,
val stat: StatInfo
)
一切正常,跑得好好的。然后某天一个同事来“清理代码”,觉得 @SerializedName("base_stat") 这行看起来是多余的——毕竟属性名就叫 baseStat,JSON里的key是 base_stat,这俩“差不多”嘛。于是删掉了:
data class PokemonStat(
val baseStat: Int, // @SerializedName 被删了
val stat: StatInfo
)
编译通过,没有报错,没有红色波浪线。同事自信地跑了一下应用——然后每个宝可梦的 baseStat 都变成了 0。不是真实的45,也不是49,就是0。所有宝可梦,全部0。
更诡异的是:Gson没有抛出任何异常,没有打印任何警告,连个日志都没有。就像什么都没发生过一样。
如果你能立刻说出为什么,那你对Gson的理解已经超过大多数初级开发了。这道题也是面试中的经典陷阱。
Gson到底是怎么匹配字段的?
这是整件事的核心。Gson在解析JSON时,做的事情其实特别简单粗暴:
- 从JSON里读一个key——比如
base_stat - 在Kotlin类里找一个名字完全一样的属性
- 找到了?把值填进去。没找到?跳过,保持默认值,继续下一个key
对,Gson的匹配规则就是精确字符串匹配,没有模糊匹配,没有大小写忽略,没有下划线转驼峰。名字不相同,就不干活。
现在回头看那个“清理后”的类:
- JSON里的key是
base_stat - Kotlin属性叫
baseStat
这两个字符串完全不相等。Gson在类里找 base_stat,找了一圈没找到,于是耸耸肩,把 baseStat 留在默认值0,然后继续解析下一个字段去了。
@SerializedName("base_stat") 从来就不是冗余代码。 它是一张便利贴,告诉Gson:“这个Kotlin属性叫 baseStat,但你在JSON里要找 base_stat 这个key。” 把便利贴撕掉,Gson就找不到对应关系了。
修复方法有两个:
- 把属性名改成
base_stat——能工作,但破坏了Kotlin的驼峰命名规范,看着别扭 - 把
@SerializedName("base_stat")加回去——既保持了优雅的命名,又能正确匹配。这才是正确做法
为什么是0,而不是直接崩溃?
我第一次遇到这个问题时,最让我震惊的不是值变成0,而是Gson居然不报错。一个字段匹配不上,按常理来说应该抛个异常,或者至少打个警告吧?但Gson就是不吭声。
Gson对待缺失key的态度是:允许缺失。它会正常构造对象,把能匹配的字段填上,匹配不上的就留在默认值。
你可以把Gson想象成在填一张纸质表格:某个格子空着不填,没问题啊,表格还是完整的。它不会因为一个空格就把整张表撕了。
具体来说,不同类型的默认值如下:
| Kotlin类型 | JSON key缺失时的默认值 |
|---|---|
Int, Long |
0 |
Double, Float |
0.0 |
Boolean |
false |
String, 任意对象, List |
null |
所以一个缺失的 Int 静默变成0,一个缺失的 String 静默变成 null。没有任何提示。
那Gson什么时候才会抛出异常?
Gson只在两种情况下会严格报错——注意,缺失key不在其中:
1. JSON格式错误
JSON本身语法有问题,比如少了一个 },多了一个逗号,或者引号没闭合。这就像你递给Gson一张被撕成两半的表格,它根本没法读,所以直接抛异常。
2. 类型不匹配
JSON里的key存在,但值的类型和Kotlin属性对不上。比如你定义了一个 Int,但JSON给的是 "base_stat": "hello"。这就像在“年龄”那一栏填了“香蕉”,Gson尝试转换但失败了,于是抛异常。
记住这个规则:Gson对缺失数据很宽容,对格式错误和类型错误很严格。 空格子没问题,但表格撕了或者在数字框里写香蕉,不行。
最可怕的部分:Gson vs Kotlin的空安全
Kotlin最大的卖点之一就是空安全。当你写:
val name: String // 没有问号
Kotlin保证这个变量永远不可能是null。编译器会强制检查,你永远不需要做空判断。这个保证是Kotlin能干掉大部分NullPointerException的原因。
但问题来了:Gson不尊重这个保证。
Gson不是通过正常的Kotlin构造器来创建对象的。它用反射偷偷溜进去,直接把值塞进字段里,完全绕过了Kotlin的检查机制。所以如果JSON里某个key缺失了,Gson会默默地把 null 放进一个声明为“非空”的 String 字段里——没有任何错误,那个“永不为null”的标签还好好地挂在那里。
然后,在代码的某个遥远角落,你做了最自然的事情:
val length = pokemon.name.length // name 其实是 null
💥 NullPointerException —— 而且崩溃的地方离JSON解析代码十万八千里。凌晨两点调试这种bug,那酸爽。
这就是陷阱:Kotlin承诺这个字段永远不会是null,Gson在背后偷偷打破了承诺。
这也是为什么从JSON来的字段,最好声明为可空类型:
data class Sprites(
@SerializedName("front_default")
val frontDefault: String? // 诚实一点
)
那个 ? 告诉Kotlin真相——这个字段真的可能为null——于是编译器会强制你在使用前处理空的情况,而不是相信一个Gson根本不会遵守的承诺。你等于在炸弹还没装好引信的时候就把它拆了。
面试版一句话总结
如果面试官问你Gson和Kotlin的相关问题,把这几点记住就够了:
Q: Gson怎么把JSON匹配到Kotlin属性?
精确的属性名匹配,或者用 @SerializedName 来映射不同的JSON key。没匹配上就用默认值。
Q: 字段返回0/null,但没有报错,为什么?
JSON里的key和属性名不匹配。Gson不认为缺失key是错误——字段保持默认值(Int是0,String是null)。
Q: Gson什么时候会抛异常?
JSON格式错误,或者类型不匹配。缺失key不会抛异常。
Q: 非空Kotlin类型有什么风险?
Gson通过反射绕过Kotlin的空安全机制,可以把null塞进非空字段,导致延迟崩溃。建议把来自JSON的字段声明为可空类型,尤其是API可能省略某些字段的时候。
这些知识点不是让你死记硬背Gson的API文档。核心是理解:Gson偏偏在你最希望它报错的地方保持沉默——而正是这种沉默,让这类bug变得特别隐蔽、特别难查。下次看到所有数字都是0或者莫名其妙出现NPE,先检查一下你的 @SerializedName 还在不在。