如何正确编解码JTextArea中的本地特殊字符(ś/ć/ó等)
看起来你遇到的问题核心是字节流和字符编码的处理逻辑错误——你的输入流把UTF-16字符直接当作单字节存储,输出流又把单个字节直接当作UTF-16字符显示,这对于多字节的UTF-8字符(比如ś、ć、ó)来说必然会导致乱码和替换字符(�,对应UTF-8的65535)。下面是具体的问题分析和修复方案:
问题根源拆解
输入流的错误处理:
你用KeyEvent.getKeyChar()获取的是UTF-16编码的char类型字符,但InputStream是字节流,需要将字符编码为UTF-8字节序列后再存入缓冲区。而你直接把char转成int存入,相当于把每个UTF-16代码单元当作单字节处理,这对于多字节UTF-8字符来说完全错误。输出流的错误处理:
ConsoleOutputStream直接把OutputStream.write(int b)中的字节转成char追加到文本框,这相当于把单个UTF-8字节当作UTF-16字符解析。而UTF-8中像ś这样的字符是由多个字节组成的,单独解析每个字节必然会出现乱码。
修复方案
1. 修正输入流(ConsoleInputStream)
我们需要将字符正确编码为UTF-8字节序列,再存入缓冲区,同时用更适合的keyTyped事件处理字符输入:
import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import javax.swing.JTextArea; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; public class ConsoleInputStream extends InputStream implements KeyListener { private final BlockingDeque<Byte> mBuffer = new LinkedBlockingDeque<>(); private final JTextArea mJTextArea; public ConsoleInputStream(JTextArea jTextArea) { mJTextArea = jTextArea; mJTextArea.addKeyListener(this); } @Override public void keyTyped(KeyEvent e) { char c = e.getKeyChar(); // 过滤无效字符(比如功能键) if (c == KeyEvent.CHAR_UNDEFINED) { return; } // 处理回车:清空输入区并写入换行符 if (c == '\n') { mJTextArea.setText(""); } // 将字符编码为UTF-8字节序列,存入缓冲区 try { byte[] utf8Bytes = String.valueOf(c).getBytes(StandardCharsets.UTF_8); for (byte b : utf8Bytes) { mBuffer.add(b); } } catch (Exception ex) { ex.printStackTrace(); } } @Override public void keyPressed(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) {} @Override public int read() throws IOException { try { // 返回无符号字节(0-255),符合InputStream规范 return mBuffer.take() & 0xFF; } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复线程中断状态 return -1; } } @Override public int read(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } int bytesRead = 0; while (bytesRead < len) { Byte nextByte = mBuffer.poll(); if (nextByte == null) { break; // 缓冲区无更多字节 } b[off + bytesRead] = nextByte; bytesRead++; } return bytesRead == 0 ? -1 : bytesRead; } }
2. 修正输出流(ConsoleOutputStream)
我们需要收集输出字节,统一解码为UTF-8字符串后再追加到文本框,同时确保UI更新在EDT线程中执行:
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import javax.swing.JTextArea; import javax.swing.SwingUtilities; public class ConsoleOutputStream extends OutputStream { private final JTextArea mJTextArea; private final ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream(); private final Charset charset = StandardCharsets.UTF_8; public ConsoleOutputStream(JTextArea jTextArea) { mJTextArea = jTextArea; } @Override public void write(int b) throws IOException { byteBuffer.write(b); } @Override public void flush() throws IOException { super.flush(); // 将收集的字节解码为UTF-8字符串 String outputText = byteBuffer.toString(charset.name()); byteBuffer.reset(); // 在EDT线程中更新UI,避免线程安全问题 SwingUtilities.invokeLater(() -> mJTextArea.append(outputText)); } @Override public void close() throws IOException { flush(); // 关闭前确保所有数据都输出 super.close(); } }
3. 启动代码无需修改
你的启动代码CommandInterface.get().start(ui.getConsoleIn(), new PrintStream(ui.getConsoleOut()));已经正确指定了输入输出流,PrintStream默认会用UTF-8编码输出,和我们的流实现兼容。
为什么这样修复有效?
- 输入流:将每个字符编码为对应的UTF-8字节序列,确保多字节字符被拆分成正确的字节存入缓冲区,
Scanner用UTF-8解码时就能还原出正确的字符。 - 输出流:收集所有输出字节后统一解码为UTF-8字符串,避免了单个字节解析的错误,同时用
SwingUtilities.invokeLater确保UI操作在EDT线程中执行,符合Swing的线程安全规范。
内容的提问来源于stack exchange,提问作者Jan Undrych

