ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Java字符编码与乱码排查:从原理到实战

2026/10/7 12:08:36 拓冰建站 浏览量
Java字符编码与乱码排查:从原理到实战 1. 从一次幽灵乱码排查说起乱码问题为什么这么难缠大概两个月前有个刚入职的同事找我求助。他写了一个接口往数据库里插一条用户昵称代码在本地测试一切正常中文显示得漂漂亮亮。结果一部署到测试环境整个团队看到他弹出的中文全变成了???。他跟我分析了一通一会儿怀疑是数据库表字符集的问题一会儿怀疑是Tomcat配置的问题折腾了一个下午改了一堆配置最后连登录用户名都跟着乱码了。这种场景做后端的朋友应该都不陌生。Java里的乱码问题大概是最容易让新手抓狂、也让有经验的工程师偶尔翻车的一类问题。难缠的点在于乱码的症状可能出现在任何一个环节但病根往往在另一个环节。有时候你盯着数据库字符集看了一上午最后发现其实是项目编译期的编码设错了有时候你在代码里逐行打印日志排查最后发现压根就是操作系统的默认字符集跟你开了个玩笑。这篇博文我打算把Java字符编码这件事从原理到实操捋一遍。不是那种干巴巴的语法罗列而是把我在项目中实际踩过的坑、排查过的现场、还有那些真正解决问题的套路都写出来。不论你是刚入门、被乱码折磨到想摔键盘还是已经有一定经验、想系统性地补全这块知识这篇文章应该都能帮上忙。先说一句结论Java里的编码问题90%都围绕着一个点——字节和字符之间的转换。这个转换发生在什么时候用了什么字符集决定了你的数据是走完全程还是半路变成一串问号。理解了这个点乱码对你来说就不再是玄学。2. Java编码机制的底层逻辑String、字节与字符集的三方博弈想解决问题绕不开对底层机制的理解。Java的String对象内部是用char数组存储的每个char对应一个UTF-16编码单元。这句话你得先记住——Java内存里的字符串始终是Unicode准确说是UTF-16编码形式。但是你的字符串最终要落到硬盘上、网络报文里、数据库表中或者显示在控制台上。这些场景几乎都不认UTF-16。它们认的是UTF-8、GBK、ISO-8859-1之类的字节编码。于是每一次字符串的输入和输出本质上都是一次字节数组 String的转换。转换的核心方法就那几个String.getBytes(Charset)把字符串转成指定字符集的字节数组。new String(byte[], Charset)把指定字符集的字节数组解码成字符串。听起来简单但问题就出在指定字符集这四个字上。如果你不指定Java就会用一个默认字符集。这个默认值取决于JVM启动时取到的操作系统区域设置以及你启动Java时有没有加-Dfile.encoding...参数。这就导致了第一个大坑:同一个代码换一台电脑跑结果可能完全不一样。我举个生活化的例子。假设你要写一封中文信信里的每个字你都在心里想好了相当于Java内存里的Unicode字符。现在你要寄出去得先按某种规则把这封信的汉字笔画拆解成代码相当于getBytes快递员按这个规则把这封信送到收信人手上。收信人收到一叠笔画代码之后如果他能按你当初的拆解规则重新组装就能看懂信相当于new String(str, charset)。如果他组装时用了另一套规则信里的字就会变成鬼画符。乱码的本质就是写信人用的规则和收信人用的规则不一致。更麻烦的是有些规则比如ISO-8859-1压根不认识汉字拆解的时候直接把汉字变成63问号的ASCII码这种就叫不可逆的损坏。数据进入系统的那一刻如果就被损坏了后面再怎么配置也是白搭。明白了这个机制你就会看懂一个很经典的面试题String s 你好; byte[] b s.getBytes(ISO-8859-1); String s2 new String(b, UTF-8); System.out.println(s2);输出是什么是乱码。因为ISO-8859-1字符集里压根没有汉字getBytes(ISO-8859-1)这一步每个中文字符都被转成了63问号信息在转换的那一瞬间就已经丢失了。这时候你后面用什么字符集解码都救不回来。所以大家经常听到一句话ISO-8859-1被人戏称为黑洞编码任何数据经过它都可能变成问号并且无法还原。还有个知识点也需要掌握UTF-8的字节序列里英文字符ASCII范围是单字节的跟ASCII编码完全兼容中文字符则是3字节。GBK中英文是单字节、中文是双字节。所以同一个字符串在不同字符集下字节长度和内容可以完全不一样。做签名校验、做报文长度计算的时候如果两端用了不同的字符集就会出现你死也想不到的诡异现象——比如前端的签名一直对不上后端的验签最后发现是getBytes()没指定字符集两边默认编码不同。2.1 源文件编码、编译器编码与运行期默认编码的层层叠加这里还有个容易忽略的环节就是Java源文件本身。javac在编译.java文件时需要先把源文件里的字符读出来然后转成内部表示UTF-16存进.class文件。如果你用UTF-8保存源文件但javac却以为你用的是GBK那你代码里所有的中文字符串字面量在编译阶段就已经变成乱码存进class字节码了。这种乱码你跟它怎么折腾运行期配置都没用因为污染源在编译这一步。另外要区分一个概念Java运行时的默认字符集和Java源文件编译时用的字符集是两码事。javac默认按平台默认字符集解析源文件但你可以用-encoding UTF-8参数强制指定。对于工程项目主流做法是统一源文件保存为UTF-8编译参数也显式指定UTF-8。有些老项目在Windows上开发IDE默认用了GBK后来迁到Linux服务器上用UTF-8编译全部中文注释和字符串字面量就哗啦啦全变乱码了这种事故的根源就在这一步。2.2 控制台乱码的特殊性System.out与System.err的编码差异控制台输出乱码也是高频问题。System.out是一个PrintStream它内部会做一次编码转换。你运行程序时向stdout输出字符串这个字符串会被转成JVM默认字符集或通过System.setOut指定的PrintStream的编码再输出。如果IDE控制台解析stdout字节流的字符集和JVM输出时用的字符集不一致你就会看到控制台里一片混乱。还有个冷知识在很多场景下System.out和System.err的编码行为可能不一致。比如有些环境给stdout和stderr设置了不同的编码导致你重定向日志时正常日志和异常堆栈用的是不同的编码规则日志文件里中文一会正常一会乱排查起来很迷惑。另外一个值得注意的点是PrintStream在写字节的时候如果遇到不能映射的字符它不会抛异常而是悄悄把它替换成替代字符通常是?。这就是为什么控制台乱码有时候表现为一堆问号而且程序本身完全没有任何报错。Java的设计理念是安静地报错但在编码这种事情上这种安静反而害得人更难定位问题。3. 乱码现场的分层定位法源头、传输、显示三层排查乱码排查最怕的就是头疼医头、脚疼医脚。我这里给出一套自己一直在用的排查思路可以把它叫做三层定位法。第一层源头层。先确认数据最早是从哪儿进来的进入系统的时候使用的是哪个字符集。比如前端表单提交的请求体在HTTP传输时用的什么编码文件读取时用的是文件的真实编码还是代码里指定的编码数据库表里存的字节序列是什么编码这一层的核心是确认数据以字节形式进入系统的那一刻字节的真实编码是什么。第二层传输层。数据在系统内部流转时经历了哪些转换。比如请求从Servlet容器到Spring MVC的参数绑定有没有做编码过滤字符串在调用getBytes()时指定了什么字符集两次转换之间是否有会吞掉异常、悄悄替换字符的过程这一层是编码问题最多发的地带因为很多框架的默认行为并不总是我们期望的。第三层显示层。数据已经以字节形式输出到了某个介质上但显示这个介质的地方用什么编码去解码。比如控制台执行程序显示终端用什么编码浏览器解析HTML页面时用了什么编码数据库客户端工具连接数据库时连接参数指定的编码是什么显示层的错误其实经常是没错误——数据本身是对的字节一点没坏但显示的地方解码用错了字符集。这种乱码最冤改一个终端设置或者浏览器设置就能好结果很多人跑去改代码。整个排查链路中最关键的一个决策点是判断乱码到底是发生在哪一层。我的建议是在任何环节之间加一个断点把字符串转成字节数组按UTF-8和GBK两种常见编码各打印一遍十六进制看看字节内容到底是什么再决定下一步。举个例子。如果你在代码里打印一个字符串System.out.println(中文测试);控制台显示乱码。你怎么判断是输出转换坏了还是终端解码坏了最简单的办法把字符串写成UTF-8的字节然后再转成一个用ISO-8859-1解码的字符串打印出来观察。如果你看到的是䏿–‡这类带有字母和奇怪拉丁符号的乱码几乎可以断定你的字符串内容本身是完好的UTF-8字节序列只是被某种单字节字符集比如ISO-8859-1错误地解码了。而如果你看到????那说明在某个环节字符已经被替换成问号信息可能已经丢失这个方向的排查重点就该放在数据源头的读取环节。4. 高频乱码场景的代码级修复方案接下来是实操环节。我挑几个实际工作中最高频的乱码场景逐一把修复方案、配置和代码写清楚。4.1 场景一读取文件乱码——先识别文件真实编码再读取文件读取乱码根源几乎都是文件真实编码与读取时指定的编码不一致。常见的有两种情况老一代Windows记事本默认保存为ANSI在简体中文系统上实际是GBK而代码里用了UTF-8读取或者反过来文件明明是UTF-8代码却用了GBK。正确的读取步骤有两步。第一步是判断文件的真实编码。如果你有把握知道编码直接指定如果没把握需要探测。探测编码用一个轻量级库就行行业内比较常用的是juniversalchardet它其实是Mozilla的chardet算法的Java移植版或者你用万金油的方式先按UTF-8读一遍如果解码过程出现异常就回退到GBK。后者在大多数业务场景下够用。判断完编码之后读取文件时要显式指定这个编码String filePath /path/to/file.txt; String charsetName UTF-8; // 根据探测结果确定 try (BufferedReader reader new BufferedReader(new InputStreamReader(new FileInputStream(filePath), charsetName))) { String line; while ((line reader.readLine()) ! null) { System.out.println(line); } }我见过有人用下面这种写法踩坑BufferedReader reader new BufferedReader(new FileReader(filePath));FileReader是便捷类——它的默认行为依赖平台默认字符集看起来省事但一旦换环境或者文件编码变了你的代码就是个定时炸弹。凡是涉及文件读写的场景我都建议避开FileReader和FileWriter直接用InputStreamReader/OutputStreamWriter显式指定字符集。4.2 场景二HTTP请求乱码——从Tomcat到Spring MVC的参数编码设置HTTP请求乱码的链条比较长。以最经典的Tomcat Spring MVC应用为例整条链路要设好几个地方。第一层Tomcat的URI编码决定了GET请求URL参数用什么字符集解码。现在的主流版本里Tomcat的默认URI编码是UTF-8但老版本或者某些特殊配置下可能就是ISO-8859-1。这个可以在server.xml的Connector节点上显式配置Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /不给URIEncoding设置的话Tomcat 8以上会默认使用UTF-8但你如果是比较老的环境建议检查一下这个配置不要指望默认值永远符合预期。第二层请求体的解码。POST表单提交如果没有显式设置Content-Type里的charsetTomcat会用什么解码呢答案是ISO-8859-1。这就是为什么很多人会在Spring MVC的配置里加一个CharacterEncodingFilterBean public FilterRegistrationBeanCharacterEncodingFilter encodingFilter() { FilterRegistrationBeanCharacterEncodingFilter registration new FilterRegistrationBean(); registration.setFilter(new CharacterEncodingFilter(UTF-8, true, true)); registration.addUrlPatterns(/*); return registration; }第一个参数encoding不用说第二个forceEncoding设为true的意思是无论请求里有没有带charset都强制用UTF-8解码。第三个参数forceRequestEncoding、forceResponseEncoding在部分Spring版本里分开了总之设置force true就是为了不让浏览器或者客户端瞎传一个错误的编码值影响全局。第三层Spring自身的消息转换器。对于JSON格式的请求体Spring用的是MappingJackson2HttpMessageConverter它默认用UTF-8理论上没问题。但如果你用了StringHttpMessageConverter处理纯文本请求体它的默认字符集是ISO-8859-1这就埋了个大坑你用RequestBody String接收一段纯文本中文框架默认按ISO-8859-1解码中文直接变问号。解决方案是显式注册一个新的StringHttpMessageConverter强制指定UTF-8。4.3 场景三数据库读写乱码——连接参数、表字段、客户端三位一体协同数据库乱码是个三明治问题从上到下被三层夹击——客户端连接参数的编码、数据库/表/字段的字符集、数据库客户端工具的解码字符集。三层任何一个地方不一致就可能出乱码。以MySQL为例JDBC连接URL里要显式带上编码参数jdbc:mysql://localhost:3306/my_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai这个characterEncodingUTF-8指定了客户端发送给MySQL的SQL语句和读取结果的编码。MySQL服务器端的character_set_server、character_set_database、character_set_connection这些变量也要互相匹配。在MySQL里你可以先跑一句SHOW VARIABLES LIKE %character_set%;查看当前系统变量排查的时候不要光看配置要用这个命令确认运行时实际生效的值。还有一个很经典的半乱码场景数据写进去是全好的查出来也全好但是通过某个可视化工具比如Navicat看表里的中文发现是一堆???。这种一般不是数据坏了而是数据库连接工具连接的编码不对。Navicat里右键编辑连接 - 高级 - 编码把编码设置为UTF-8重新查询就正常了。注意这只是显示问题不代表数据被改坏。4.4 场景四部署到Linux服务器后的乱码——JVM启动参数与文件系统编码本地Windows开发一切正常部署到Linux服务器就乱码这个问题在企业里出现频率极高。原因通常集中在两方面。一个是JVM的默认字符集带了过去。如果你在Linux上启动Java时没有指定-Dfile.encodingUTF-8而代码里又有某些地方依赖了默认字符集那Linux上JVM的默认字符集可能和开发机不一样行为就会不一致。稳妥的做法是在启动脚本里统一加上java -Dfile.encodingUTF-8 -jar app.jar这里多说一句很多在JDK 8上运行正常的项目升级到JDK 17之后出现莫名其妙的乱码就是因为在JDK 18之前-Dfile.encoding虽然能影响很多IO操作但Charset.defaultCharset()并不总是跟随这个参数变化环境一变、JDK版本一换老代码的隐患就爆出来了。JDK 18之后-Dfile.encodingUTF-8成了默认行为反而能消掉很多问题但也让一些靠着旧默认字符集歪打正着跑通的系统暴露了真实问题。所以我的建议一直是永远不要在代码里依赖默认字符集该指定就显式指定。另一个容易忽略的是Linux系统的locale配置和文件系统的文件名编码。如果你的代码读取了一个中文文件名的文件File.listFiles()返回的文件名在Java内部是正常的Unicode但如果你的Linux系统没有装中文字体或locale配置不对某些依赖底层系统调用的场景比如调用OS命令传文件名就会出问题。这种场景虽然不是典型的字符编码问题但表现和编码乱码类似排查时容易走弯路。4.5 场景五URL中文参数乱码——编码两次解码一次的正确姿势还有一类高频问题URL里带中文参数前端请求过来后端收到乱码。不管你是GET还是POST只要参数不是ASCII字符就涉及URL编码。正确的做法是发送端对中文做URL编码encode接收端对编码过的内容做URL解码decode。为什么有时候会看到编码两次解码一次因为某些框架或容器在收到数据后会先自动解码一次如果发送端也编码了一次那后端就得解码两次才能还原。Java侧发送请求时String params URLEncoder.encode(中文, UTF-8); String url http://example.com/api?keyword params;接收端解析时String keyword URLDecoder.decode(request.getParameter(keyword), UTF-8);这里要注意在Spring MVC里request.getParameter()返回的值。当你配置了URIEncodingUTF-8当前Tomcat默认之后URL里编码过的中文在容器层已经被解码成了Unicode字符串你拿到参数之后直接就是中文不需要再手动URLDecoder.decode()。如果再解一次码中文会被当成编码串再解一遍可能解出一个不伦不类的结果——这就是为什么我都按教程解码了还是乱码的常见原因。所以写代码之前先确认你的容器到底帮你解过码没有。5. 代码层面的核心细节getBytes、new String与Charset的六个关键决策点方案都写完了这里再把代码层面那些容易踩坑的细节拎出来一个个说透。5.1 用不带参的getBytes()之前想清楚默认字符集是什么你好.getBytes()看起来人畜无害实际上结果完全取决于JVM运行环境的默认字符集。在UTF-8环境它是6个字节在GBK环境它是4个字节。如果你这段代码是给某个平台用的并且那个平台环境可控那还能碰碰运气如果这段代码会被不同环境的人调用出问题的概率就是100%。给所有getBytes()调用都补上字符集参数是我做代码审查时最先扫的一类问题。5.2 不要用String(byte[])这种裸构造器跟上面同理new String(bytes)同样依赖默认字符集。如果你明确知道字节是什么编码必须写new String(bytes, UTF-8)这类形式。不知道编码的情况宁可不转先探测一下再说。5.3 对二进制数据做字符串化时会遇到的不可逆损失你有一个二进制数组比如一个图片文件的内容或者一段加密后的密文需要以字符串形式存放或传输。很多人会直接new String(byte[])——结果通常是灾难。因为二进制字节数组里可能有大量不合法于当前字符集的字节序列转换时要么被替换成问号要么直接导致乱码。正确的做法是用Base64String encodedStr Base64.getEncoder().encodeToString(binaryData); byte[] decoded Base64.getDecoder().decode(encodedStr);稍微注意一下Base64编码的字符串在URL里使用时要改用Base64.getUrlEncoder()因为标准的Base64会包含和/这两个URL特殊字符。5.4 Charset.forName的异常吞噬问题代码里经常写Charset.forName(UTF-8)如果字符集名写错了或者JVM不支持会抛UnsupportedCharsetException。这个名字看起来是受检异常但它是IllegalArgumentException的子类属于运行时异常不强制捕获。问题在于如果你把字符集名写成了一个变量比如从配置文件里读的万一配错了服务启动时就会炸。要尽量把字符集名写成常量并且在一处统一管理避免各处硬编码造成维护灾难。5.5 编码成字节后再用String存储的迷之双重转换有的代码会把字节数组再转成字符串存数据库比如new String(bytes, StandardCharsets.UTF_8)然后把字符串存到数据库的一个VARCHAR列里。如果数据库的列字符集是LATIN1存储时数据库服务器会再做一次从UTF-16到LATIN1的转换于是你等于是把UTF-8的字符串以UTF-16内存形态交给了数据库数据库再转换为LATIN1存储。等下次查出来编码痕迹早乱了。这种问题排查起来极其痛苦因为每个环节看起来都有转换的动作但你很难看出哪里错了。我的建议是能用二进制列存数据就存BLOB别拿字符串类型的列存编码字节。5.6 字符串比较时的Unicode规范化问题还有一个可能跟你遇到的乱码难题八竿子打不着的点但一旦遇上就特别难受Unicode的规范化Normalization。同一个字é可能有两种Unicode表示方式一种是单个code pointU00E9另一种是字母e加组合重音符号U0065 U0301。视觉上几乎一模一样但按字节比较完全不一样。数据库里一个存的是全组合形式另一个存的是预组合形式你查数据库时用WHERE name é可能死活查不到那一行。Java里可以用Normalizer.normalize(input, Normalizer.Form.NFC)来统一。如果你做的业务涉及多语言输入或者用户上传的文件名、内容需要做去重、匹配建议在入库前统一做一次规范化避免后续各种灵异查不到、匹配失败的问题。6. 面试题里关于编码的那些深水区编码这块也是Java面试里的常客。我这两年面试别人或者跟朋友闲聊时听到的经典面试题整理一下方便你有个概念公司出编码题的时候到底想考什么。第一类是为什么中文乱码发散型的回答通常考察知识广度。面试官期待的回答不是一个孤立的结论而是从String内存里是UTF-16、转换发生在字节与字符之间、默认字符集随环境变化三段式展开说明。能把源头、传输、显示三个阶段分开讲面试官的体验就会很好。第二类是具体代码题典型的就是前面举过的例子String s 苗; byte[] b s.getBytes(ISO-8859-1); System.out.println(b.length);这种题考的是你对字符集编码长度的敏感度。苗在ISO-8859-1下无法表示会变成63问号所以b.length是1。这个题很多背过八股文的人反而会答错因为容易凭直觉背出UTF-8中文占3字节这个结论。第三类是概念题比如UTF-8和GBK有什么区别Unicode和UTF-8之间的关系BOM是什么。这几个概念说起来容易能讲清楚的不多。特别注意BOMUTF-8文件可能会在开头带一个EF BB BF的字节序列作为字节序标记Java读取UTF-8文件时理论上BOM会作为不可见字符被读入字符串头部如果你在解析CSV或日志时发现第一个字段莫名其妙多了一个不可见字符我打赌十有八九就是BOM在作怪。7. 把乱码问题扼杀在源头项目规范与工程化方案最后这部分我想聊点平时没人写、但比修bug更值钱的东西。乱码问题之所以反复出现根源在于缺乏一套统一的字符编码约定。光靠遇到问题再修永远有修不完的乱码。所以我的建议是把编码规范的约束前置到开发流程里。第一统一项目和文件的编码配置。Maven或Gradle项目里强制设置project.build.sourceEncoding为UTF-8。Maven的pom.xml里加这两行properties project.build.sourceEncodingUTF-8/project.build.sourceEncoding project.reporting.outputEncodingUTF-8/project.reporting.outputEncoding /properties同时IDE编码也统一设置为UTF-8。在IDEA里Settings - Editor - File Encodings把Global Encoding、Project Encoding、Properties Files三个地方的编码全部设成UTF-8底下的勾Create UTF-8 files就留着默认。你可能会觉得这只是个人开发环境的事但一个项目如果几个人同时开发IDE编码不统一每次提交代码的时候中文注释就会以不同编码版本在Git里反复横跳diff一多就离乱码事故不远了。第二所有IO操作禁止使用依赖默认字符集的API。代码审查时凡是出现new FileReader()、new FileWriter()、字符串.getBytes()无参、new String(bytes)无参的一律要求改为显式指定字符集。可以定义一组工具方法统一封装这样即使将来要调整全项目的字符集规范也只需要改一个工具类。第三建立一套编码排查SOP标准作业程序放在团队Wiki里。一旦有人遇到乱码先按SOP分层定位而不是凭感觉乱改配置。这套SOP的核心就是我上面写的三层定位法外加一组常用的排查命令和代码片段。遇到乱码问题先明确字节到底在哪一步变成了什么样再做修改。第四数据库连接和HTTP容器的编码参数要写进项目初始化配置清单里。很多乱码事故都是项目初建时这些配置没设好等到系统跑起来数据积累了一大批才发现某个环节的编码不对这时候再改就涉及数据迁移甚至历史数据回读代价翻倍。我自己的习惯是做一个Charsets常量类把项目里所有用到的字符集名集中定义public final class Charsets { public static final Charset UTF_8 StandardCharsets.UTF_8; public static final Charset GBK Charset.forName(GBK); public static final Charset ISO_8859_1 StandardCharsets.ISO_8859_1; private Charsets() {} }所有代码里统一引用这个类不用字符串散写。这样以后万一要全局排查或者替换字符集一个文件改完全项目生效。8. 单条经验乱码问题排查时先问自己三个对不对我最后分享一条实战体会它的价值可能比前面所有配置都高。每次遇到乱码问题我会在动手前强制自己回答三个问题数据进来时字节的对不对这一步确认数据在入口处是否是正确编码的字节序列。转换过程中用的字符集对不对这一步确认代码里每个getBytes和new String使用的字符集是否和数据实际编码一致。显示出口处解码规则对不对这一步确认终端、浏览器、数据库客户端等显示介质是否正确地解读了输出的字节。这三句话听起来很基础但你真正对标排查的时候会发现绝大多数乱码事故卡住人都是因为第一步没确认就直接跳到第三步去猜了。先确认字节的真相再谈配置的修改。不看到实际字节长什么样你的所有猜测都只是安慰自己。实践里还有一个很实用的技巧写一个极简的编码探针接口里面可以直接返回一段中文实例的getBytes(UTF-8)的十六进制字符串需要排查时先访问一下这个接口立刻能确认当前运行环境的JVM默认编码和实际输出情况。不用每次遇到问题都重新写测试代码。我踩过的乱码坑两只手数不过来。但说实话字符编码这块知识体系一旦建起来之后就成了一劳永逸的积累。数据在Java体系里怎么流转、字节怎么转换、每一步用到什么字符集这些搞清楚了乱码对你来说就不再是埋伏在代码里的幽灵只是流程中一个可定位、可修复的普通节点罢了。