JavaのWebアプリで文字化けするのはなぜ?フォーム送信とUTF-8の関係

本記事は、Schooの「Java入門 中級【2026年版】」第4回「Webアプリケーションの動作原理と画面連携」に関連する補足記事です。フォームから送った日本語が文字化けする原因を、文字を送る側・読み取る側の対応から整理します。
「山田」と入力したのに、結果画面には別の文字や ?? が出る。そんなときは、まず「Javaが受け取った時点でおかしいのか」「Javaの値は正しく、返すときにおかしくなるのか」を分けて確認します。画面にUTF-8と書くだけでは、受信と表示の両方を設定したことにはなりません。
この記事では、同じ名前を入力・送信・表示する小さなサンプルを使います。データベースやファイルへの保存は行わず、文字コードだけを切り分けます。
Schoo コースページ:Java入門 中級【2026年版】
まず結論:受信する設定と、返す設定は別
request.setCharacterEncoding("UTF-8");
String userName = request.getParameter("userName");
response.setContentType("text/plain; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println(userName);
requestの指定は、POSTで届いた本文をどの文字コードで読むか。getParameter()より前に書きます。responseの指定は、返す文字をどの文字コードで出力するか。getWriter()より前に書きます。
これはUTF-8で送る通常のフォーム(application/x-www-form-urlencoded)の例です。上の設定は互いの代わりにはなりません。JSONやファイルアップロードの読み取りは、この記事の対象外です。
E5 B1 B1E7 94 B0%E5%B1%B1%E7%94%B0 のように表すresponse.setContentType(
"text/plain; charset=UTF-8");getWriter() で出力図はPOSTのフォーム送信と、Servletからのプレーンテキスト応答の例です。JavaのStringを「UTF-8の文字列」に変更しているのではなく、入出力で使う変換方法を指定しています。
なぜ日本語だけ文字化けするのか
通信でやり取りするのは、人が見ている文字の形ではなくバイトという数値の列です。「文字をどのバイトで表すか」というルールの1つがUTF-8です。受け取った側も同じルールで読んで、元の文字に戻します。
たとえばUTF-8では、「山」は E5 B1 B1、「田」は E7 94 B0 というバイトで表されます。ここで使っている英数字はバイトを16進数で書いたものです。暗記は不要で、送るときと読むときのルールがそろう必要があることが要点です。
E5 B1 B1E7 94 B0入力した文字に戻る
違う文字として読み取られる
この例の右側は、JavaでShift_JISとして読んだ結果です。文字化けの見た目は、間違えた文字コードや入力した文字によって変わります。
UTF-8とShift_JISでは、基本的な半角英数字の多くは同じバイトで表せます。そのため Yamada123 では問題が見えず、「山田」を送ったときに初めて違いが現れる場合があります。英数字のテストだけで、日本語も正しく扱えるとは判断できません。
また、%E5%B1%B1 のような表示そのものは文字化けではありません。フォーム送信でバイトを % と16進数で表したものです。Servletでは通常、フォームの解析時にこの表現も処理されるため、getParameter() の結果をさらにURLデコードする必要はありません。
UTF-8を書く場所は同じ役割ではない
| 設定する場所 | 何を指定するか |
|---|---|
| エディターでHTML・JSP・JavaファイルをUTF-8保存 | ファイルの実際のバイト。宣言を書くだけでは保存形式は変わらない |
HTMLの <meta charset="UTF-8"> | そのHTML文書の文字コードをブラウザに伝える宣言 |
formの accept-charset="UTF-8" | フォーム送信に使う文字コード |
request.setCharacterEncoding("UTF-8") | POST本文から文字を読み取るための指定 |
response.setContentType("text/plain; charset=UTF-8") | 応答の種類と文字コード。出力とHTTPヘッダーに関係する |
HTMLファイルはUTF-8で保存し、宣言とそろえます。WebサーバーがHTTPヘッダーで別のcharsetを送っている場合は、metaだけを直しても解決しないことがあります。実際のファイル・HTMLの宣言・HTTPヘッダーを食い違わせないことが大切です。
Javaファイルも、エディターの保存形式とコンパイラーが読む文字コードをそろえます。コマンドでコンパイルする場合は javac -encoding UTF-8 のように指定できます。IDEではプロジェクトの文字コード設定を確認します。
設定を書く順番が遅いと、読み直しにはならない
request にsetCharacterEncoding("UTF-8")getParameter()本文を文字として読み取る
response にsetContentType(
"text/plain; charset=UTF-8")getWriter()文字を出力する準備
// 順番が逆:すでに読み取った値を変換し直す処理にはならない
String userName = request.getParameter("userName");
request.setCharacterEncoding("UTF-8");
文字コードの指定は「次にどう読むか」を決めるもので、すでに取得した userName を修復するものではありません。ログ出力のための getParameter() や、Servletより前に動く共通処理で先にパラメータを読み取っていないかも確認します。
// 順番が逆:Writerの文字コードが決まった後になっている
PrintWriter out = response.getWriter();
response.setContentType("text/plain; charset=UTF-8");
返す側でも、getWriter() を呼んだ後のcharset指定は、そのWriterの文字コードを変更しません。画面に出力する前であっても、Writerを取得する前に指定します。
共通設定ですでにUTF-8になっている環境では、指定を省いても動くことがあります。「書かないと必ず文字化けする」ではなく、誰が、どの時点で、どの文字コードを指定しているかを確認します。
動かして確認:固定の日本語と、受信した日本語を並べる
下の2ファイルをUTF-8で保存します。既存のJakarta Servlet対応プロジェクトで試す例です。サーバーやIDEの導入手順は扱いません。
入力画面:src/main/webapp/encoding-form.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>日本語の受信確認</title>
</head>
<body>
<h1>日本語の受信確認</h1>
<form action="encoding-check" method="post"
accept-charset="UTF-8">
<label for="name-field">名前</label>
<input type="text" id="name-field"
name="userName" value="山田">
<button type="submit">送信する</button>
</form>
</body>
</html>
受信処理:src/main/java/example/EncodingCheckServlet.java
package example;
import java.io.IOException;
import java.io.PrintWriter;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@WebServlet("/encoding-check")
public class EncodingCheckServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
// POST本文を読み取る前に指定する
request.setCharacterEncoding("UTF-8");
String userName = request.getParameter("userName");
// 出力用のWriterを取得する前に指定する
response.setContentType("text/plain; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("固定の文字: 山田");
if (userName == null) {
out.println("userNameという名前の値が届いていません");
} else {
out.println("受信した文字: " + userName);
}
}
}
アプリのコンテキストパスが /lesson04 なら、http://localhost:8080/lesson04/encoding-form.html を開いて送信します。ServletのURLを直接開くとGETになるため、この例のdoPostは動きません。
「山田」を送信したときの正常な表示は次のとおりです。
固定の文字: 山田
受信した文字: 山田
「固定の文字」はJavaのソースに書いた日本語、「受信した文字」はフォームから届いた日本語です。この2つを比べると、通信で受け取る部分と、結果を返す部分を切り分けやすくなります。レスポンスはHTMLではなくプレーンテキストなので、入力されたタグを実行させません。実際の利用者の秘密情報を画面やログに出さないでください。
文字化けを再現して、どちら側の問題か確かめる
以下は学習用のローカル環境で行う比較です。毎回、正常なコードに戻してから1か所だけ変え、再ビルド・反映してフォームを送信し直します。本番の設定は変更しません。
| 試す変更 | 「山田」を送ったときの確認結果 |
|---|---|
| 変更なし | 固定・受信ともに「山田」 |
requestの "UTF-8" だけを "Shift_JIS" に変更 | 固定は「山田」、受信は「螻ア逕ー」。受信時に違うルールで読んでいる |
responseのcharsetだけを ISO-8859-1 に変更 | 日本語をその文字コードで表せず ? に置き換わる。「山田」の部分は ??。固定の日本語も影響を受ける |
2番目の例では、返す文字コードは正しいUTF-8のままです。それでもJavaで取得した値がすでに違う文字なので、誤った文字がそのまま画面に表示されます。responseの指定だけでは受信時の読み間違いを直せません。
3番目の例は、別の文字として読むのではなく、日本語を表せない文字コードで出力する例です。元の文字が ? に置き換わった後では、画面の表示設定を変えても元の情報を復元できません。確認後は必ずUTF-8に戻します。
実際に困ったときの確認順
- 送信前の入力画面を確認する。見出しや初期値の日本語がすでに崩れているなら、HTMLの保存形式、meta、配信時のContent-Typeを調べます。
- 受け取ったStringを確認する。自作のテスト入力を使い、getParameter直後でデバッガーの変数を確認します。コンソール自体の文字コードが違う場合もあるため、ログの見た目だけで断定しません。
- 固定の日本語と比べる。固定だけ正しく、受信だけ崩れるなら入力・受信経路が手掛かりになります。両方崩れるなら、出力設定やソースの保存・コンパイル設定も確認します。これは絞り込みの手掛かりであり、見た目だけで原因を決めつけません。
- ブラウザのNetworkで応答を確認する。POSTのリクエストを選び、Response HeadersのContent-Typeが
text/plain;charset=UTF-8になっているかを確認します。;の後の空白や大文字小文字は表示によって異なります。 - 設定の位置と反映を確認する。受信前・Writer取得前に指定できているか、変更したクラスが再ビルドされ、実行中のアプリに反映されているかを確認します。
すでに文字化けした値に new String(...getBytes(...), ...) を付けて見た目だけ直すことは、最初の対処にしません。送信時の文字コードと最初に読み取る処理をそろえます。誤変換の途中で情報が失われると、後から元に戻せない場合があります。
GETの検索条件でも、同じ設定で直る?
request.setCharacterEncoding() はリクエスト本文の文字コードを指定します。GETでURLの ? 以降に付く検索条件は、同じ指定だけで直るとは限りません。POSTでもURL側に付けたパラメータは区別して考えます。
Tomcat 10.1ではURLの読み取りに URIEncoding が関係し、既定値はUTF-8です。GETだけで起きる問題では、送られたURLとサーバーのURL読み取り設定を確認します。POSTが直った設定を、GETにもそのまま当てはめないことがポイントです。
JSPに表示する場合は、ファイルと応答を分けて指定する
結果画面をJSPにする場合も考え方は同じです。まずは入力値を使わない短いJSPで、日本語を正しく表示できるか切り分けられます。src/main/webapp/encoding-view.jsp をUTF-8で保存します。
<%@ page pageEncoding="UTF-8"
contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>表示の確認</title>
</head>
<body>
<p>山田</p>
</body>
</html>
pageEncoding="UTF-8":サーバーがJSPのソースファイルを読むときの文字コード。contentType="text/html; charset=UTF-8":JSPが返すHTMLの種類と文字コード。
このJSPの設定は、POSTで届いた名前を読む設定の代わりではありません。ServletからJSPへ表示を引き継ぐ場合も、入力値はServletで正しく取得してから渡します。JSPが直接getParameterで読む例なら、その読み取りより前にrequest側を設定します。ここでは設定の役割だけを確認し、JSPの文法には立ち入りません。
確認問題
フォームはUTF-8で保存され、accept-charset="UTF-8" を指定したPOST送信とします。次のコードについて考えてください。
String userName = request.getParameter("userName");
request.setCharacterEncoding("UTF-8");
PrintWriter out = response.getWriter();
response.setContentType("text/plain; charset=UTF-8");
out.println(userName);
- 受信側と返す側について、設定が適用される順番にコードを書き直してください。
- 本文の正常なサンプルから、requestの指定だけをShift_JISへ変えて「山田」を送信しました。固定の文字と受信した文字はそれぞれどうなりますか。responseをUTF-8に指定し直すだけで直りますか。
- JSPのpageEncodingとcontentTypeは、それぞれ何の文字コードを指定しますか。どちらか一方がPOST本文の受信設定になりますか。
- サンプルのPOST送信では正しく表示されるのに、別のGET検索画面でだけ文字化けします。request.setCharacterEncodingの追加だけで解決すると言い切れますか。理由を説明してください。
演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。
まとめ
- 文字化けは、送る側と読む側のルールの不一致などによって起こる。
- POSTの受信設定はパラメータを読む前、応答の文字コードはWriterを取得する前に指定する。
- HTMLの宣言・ファイルの保存形式・受信設定・応答設定は役割が違う。
- 固定の日本語と受信した日本語を比べ、どの段階でおかしくなったかを絞り込む。