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

最終更新日

Schoo Java入門 中級 第4回 Webアプリケーションの動作原理と画面連携

本記事は、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やファイルアップロードの読み取りは、この記事の対象外です。

図解:送るときと返すときに、文字とバイトを変換する
ブラウザ:入力画面
名前
山田
この文字をUTF-8で送信する
① ブラウザが文字をバイトにする
送信する「山田」の6バイト
E5 B1 B1E7 94 B0
フォーム本文では、それぞれのバイトを %E5%B1%B1%E7%94%B0 のように表す
② サーバーがUTF-8として読み取る
Servlet:受信側
request
.setCharacterEncoding(
"UTF-8");
その後に getParameter()
userName の値は 山田
③ サーバーが返す文字もUTF-8のバイトにする
Servlet:返す側
response.setContentType(
"text/plain; charset=UTF-8");
その後に getWriter() で出力
HTTPヘッダーでも、返す文字コードを伝える
④ ブラウザがUTF-8として読み取る
ブラウザ:結果画面
受信した文字: 山田

図はPOSTのフォーム送信と、Servletからのプレーンテキスト応答の例です。JavaのStringを「UTF-8の文字列」に変更しているのではなく、入出力で使う変換方法を指定しています。

なぜ日本語だけ文字化けするのか

通信でやり取りするのは、人が見ている文字の形ではなくバイトという数値の列です。「文字をどのバイトで表すか」というルールの1つがUTF-8です。受け取った側も同じルールで読んで、元の文字に戻します。

たとえばUTF-8では、「山」は E5 B1 B1、「田」は E7 94 B0 というバイトで表されます。ここで使っている英数字はバイトを16進数で書いたものです。暗記は不要で、送るときと読むときのルールがそろう必要があることが要点です。

図解:同じバイトでも、読むルールが違うと別の文字になる
UTF-8で送った「山田」
E5 B1 B1E7 94 B0
UTF-8で読む
山田
入力した文字に戻る
Shift_JISで読む
螻ア逕ー
違う文字として読み取られる

この例の右側は、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ではプロジェクトの文字コード設定を確認します。

設定を書く順番が遅いと、読み直しにはならない

図解:指定が必要なのは「読み取る前」と「出力を準備する前」
受信する順番
1. request
setCharacterEncoding("UTF-8")
2. getParameter()
本文を文字として読み取る
返す順番
1. response
setContentType(
"text/plain; charset=UTF-8")
2. 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に戻します。

実際に困ったときの確認順

  1. 送信前の入力画面を確認する。見出しや初期値の日本語がすでに崩れているなら、HTMLの保存形式、meta、配信時のContent-Typeを調べます。
  2. 受け取ったStringを確認する。自作のテスト入力を使い、getParameter直後でデバッガーの変数を確認します。コンソール自体の文字コードが違う場合もあるため、ログの見た目だけで断定しません。
  3. 固定の日本語と比べる。固定だけ正しく、受信だけ崩れるなら入力・受信経路が手掛かりになります。両方崩れるなら、出力設定やソースの保存・コンパイル設定も確認します。これは絞り込みの手掛かりであり、見た目だけで原因を決めつけません。
  4. ブラウザのNetworkで応答を確認する。POSTのリクエストを選び、Response HeadersのContent-Typeが text/plain;charset=UTF-8 になっているかを確認します。; の後の空白や大文字小文字は表示によって異なります。
  5. 設定の位置と反映を確認する。受信前・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);
  1. 受信側と返す側について、設定が適用される順番にコードを書き直してください。
  2. 本文の正常なサンプルから、requestの指定だけをShift_JISへ変えて「山田」を送信しました。固定の文字と受信した文字はそれぞれどうなりますか。responseをUTF-8に指定し直すだけで直りますか。
  3. JSPのpageEncodingとcontentTypeは、それぞれ何の文字コードを指定しますか。どちらか一方がPOST本文の受信設定になりますか。
  4. サンプルのPOST送信では正しく表示されるのに、別のGET検索画面でだけ文字化けします。request.setCharacterEncodingの追加だけで解決すると言い切れますか。理由を説明してください。

演習後の確認:こちらのページを使って、自分で考えた内容を見直せます。

まとめ

  • 文字化けは、送る側と読む側のルールの不一致などによって起こる。
  • POSTの受信設定はパラメータを読む前、応答の文字コードはWriterを取得する前に指定する。
  • HTMLの宣言・ファイルの保存形式・受信設定・応答設定は役割が違う。
  • 固定の日本語と受信した日本語を比べ、どの段階でおかしくなったかを絞り込む。

関連する記事

シェアする