Javaのsubstringとは、文字列の一部を切り出して新しい文字列として返すStringクラスのメソッドで、substring(開始位置) と substring(開始位置, 終了位置) の2つの形があります。 位置は0から数え、終了位置の文字は結果に含まれません。末尾N文字は s.substring(s.length() - n) で取り出せます。

この記事では、よく使う切り出し方を早見表にまとめたうえで、StringIndexOutOfBoundsException を3つのパターンで再現し、実際のエラーメッセージと直し方を示します。エラーメッセージは Java のバージョンで文面が変わるため、Java 8・11・17・21 の4つで同じコードを実行して比べました。文字数が足りなくても止まらないユーティリティ、絵文字(サロゲートペア)を壊さずに切る方法も、コピーして使える形で載せています。

substringの早見表

s.substring(begin, end) は「begin 番目から end 番目の手前まで」 を返します。取り出される文字数は end - begin です。

やりたいこと 書き方 s = "ORD-2026-000123" の結果
先頭 N 文字 s.substring(0, n) n=3 → ORD
末尾 N 文字 s.substring(s.length() - n) n=6 → 000123
N 文字目以降(1から数えて) s.substring(n - 1) n=5 → 2026-000123
N 文字目から M 文字 s.substring(n - 1, n - 1 + m) n=3, m=4 → D-20
先頭 1 文字を除く s.substring(1) RD-2026-000123
末尾 1 文字を除く s.substring(0, s.length() - 1) ORD-2026-00012
末尾から6文字目〜4文字目 s.substring(s.length() - 6, s.length() - 3) 000

区切り文字を基準に切り出すときは、indexOf や lastIndexOf と組み合わせます。

やりたいこと 書き方 入力 → 結果
区切り文字の前 s.substring(0, s.indexOf('@')) taro.yamada@example.com → taro.yamada
区切り文字の後 s.substring(s.indexOf('@') + 1) taro.yamada@example.com → example.com
最後の区切り文字の後 s.substring(s.lastIndexOf('/') + 1) /home/taro/docs/report.final.pdf → report.final.pdf
拡張子 s.substring(s.lastIndexOf('.') + 1) /home/taro/docs/report.final.pdf → pdf
拡張子を除く s.substring(0, s.lastIndexOf('.')) /home/taro/docs/report.final.pdf → /home/taro/docs/report.final
かっこの中 s.substring(s.indexOf('(') + 1, s.indexOf(')')) [INFO] start (id=42) done → id=42
2文字以上の区切りの後 s.substring(s.indexOf("::") + 2) name::taro → taro

範囲外エラーになる条件は次の3つです。

パターン 例(s = "Hello, World"、長さ12) Java 21 のメッセージ
begin が負 s.substring(-1, 5) Range [-1, 5) out of bounds for length 12
end が length 超え s.substring(0, 13) Range [0, 13) out of bounds for length 12
begin が end より大きい s.substring(8, 3) Range [8, 3) out of bounds for length 12

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java substringの使い方 末尾切り出しと範囲外エラーの直し方」

確認に使った環境

項目 内容
主な実行環境 OpenJDK 21.0.10(Linux)。java ファイル名.java の単一ファイル実行
エラーメッセージの比較 OpenJDK 1.8.0_504、11.0.32.1、17.0.20.1、21.0.10 の4つで同じクラスを実行
日本語と絵文字の表示 java -Dstdout.encoding=UTF-8 ファイル名.java で実行
未実行 Java 22 以降(手元に実行環境がないため、この記事では扱っていません)

仕様については、Java SE 21 の API ドキュメント(String クラス)の substring と offsetByCodePoints の項を開いて、例外が投げられる条件を確認しました。

substringの基本 2つの書き方と位置の数え方

位置(インデックス)は 0 から数えます。 "Hello, World" の場合、H が 0、, が 5、空白が 6、W が 7、最後の d が 11 で、長さは 12 です。

位置 0 1 2 3 4 5 6 7 8 9 10 11
文字 H e l l o , (空白) W o r l d
public class SubstringBasic {
    public static void main(String[] args) {
        String s = "Hello, World";
        System.out.println("length = " + s.length());

        // 引数1つ:開始位置から最後まで
        System.out.println(s.substring(7));

        // 引数2つ:開始位置から、終了位置の手前まで
        System.out.println(s.substring(0, 5));
        System.out.println(s.substring(7, 12));

        // 開始位置 = 長さ、開始位置 = 終了位置 は空文字になる(エラーにならない)
        System.out.println("[" + s.substring(12) + "]");
        System.out.println("[" + s.substring(5, 5) + "]");

        // 元の文字列は変わらない
        s.substring(0, 5);
        System.out.println(s);

        // 全角文字も1文字として数える
        System.out.println("あいうえお".substring(1, 3));
    }
}
length = 12
World
Hello
World
[]
[]
Hello, World
いう

押さえておきたい点は3つです。

  • 終了位置の文字は含まれません。 substring(0, 5) は位置 0〜4 の5文字(Hello)です。
  • substring(s.length()) は空文字を返します。 開始位置が長さと同じ値まではエラーになりません。
  • String は変更できない(イミュータブルな)型なので、結果は必ず変数で受け取ります。s.substring(0, 5); だけでは s は変わりません。

先頭・末尾・N文字目以降を切り出す

早見表の書き方を、実際に動かして確かめます。

public class SubstringCheat {
    public static void main(String[] args) {
        String s = "ORD-2026-000123";
        int len = s.length();
        System.out.println("length = " + len);

        System.out.println("先頭3文字        : " + s.substring(0, 3));
        System.out.println("末尾6文字        : " + s.substring(len - 6));
        System.out.println("5文字目以降      : " + s.substring(4));
        System.out.println("3文字目から4文字 : " + s.substring(2, 2 + 4));
        System.out.println("先頭1文字を除く  : " + s.substring(1));
        System.out.println("末尾1文字を除く  : " + s.substring(0, len - 1));
        System.out.println("末尾6〜4文字目   : " + s.substring(len - 6, len - 3));
    }
}
length = 15
先頭3文字        : ORD
末尾6文字        : 000123
5文字目以降      : 2026-000123
3文字目から4文字 : D-20
先頭1文字を除く  : RD-2026-000123
末尾1文字を除く  : ORD-2026-00012
末尾6〜4文字目   : 000

末尾N文字は「長さ − N」から最後まで

末尾から数えるときは、s.length() - n を開始位置にします。 Java の substring は負の数を「末尾から」とは解釈しないので、長さから引き算して位置を求めます。

取りたい範囲 開始位置 終了位置
末尾 N 文字 s.length() - n 省略(最後まで)
末尾 N 文字を除いた部分 0 s.length() - n
末尾から a 文字目〜b 文字目(a は b より大きい) s.length() - a s.length() - b + 1

文字列が N 文字より短いと s.length() - n が負になり、範囲外エラーになります。短い文字列が来る可能性があるときは、後述の right メソッドのように Math.max(0, s.length() - n) で 0 未満にならないようにします。

「N文字目」と位置のずれに注意

人が数える「N 文字目」は 1 始まり、substring の位置は 0 始まりです。「5文字目以降」は substring(4) になります。1つずれるとエラーにはならず、結果だけが変わるので、短い文字列で一度出力を確かめると安心です。

区切り文字の前後を切り出す indexOf との組み合わせ

indexOf は、指定した文字や文字列が最初に現れる位置を返します。lastIndexOf は最後に現れる位置です。

public class SubstringIndexOf {
    public static void main(String[] args) {
        String mail = "taro.yamada@example.com";
        int at = mail.indexOf('@');
        System.out.println("@ の位置  : " + at);
        System.out.println("@ の前    : " + mail.substring(0, at));
        System.out.println("@ の後    : " + mail.substring(at + 1));

        String path = "/home/taro/docs/report.final.pdf";
        System.out.println("ファイル名: " + path.substring(path.lastIndexOf('/') + 1));
        System.out.println("拡張子    : " + path.substring(path.lastIndexOf('.') + 1));
        System.out.println("拡張子なし: " + path.substring(0, path.lastIndexOf('.')));

        String kv = "key=value=more";
        System.out.println("最初の=の後: " + kv.substring(kv.indexOf('=') + 1));

        String log = "[INFO] start (id=42) done";
        System.out.println("()の中    : " + log.substring(log.indexOf('(') + 1, log.indexOf(')')));
        System.out.println("[]の中    : " + log.substring(log.indexOf('[') + 1, log.indexOf(']')));

        String sep = "name::taro";
        System.out.println("::の後    : " + sep.substring(sep.indexOf("::") + "::".length()));
    }
}
@ の位置  : 11
@ の前    : taro.yamada
@ の後    : example.com
ファイル名: report.final.pdf
拡張子    : pdf
拡張子なし: /home/taro/docs/report.final
最初の=の後: value=more
()の中    : id=42
[]の中    : INFO
::の後    : taro

「区切り文字の後」は indexOf(...) + 区切りの長さ が開始位置です。1文字の区切りなら + 1、"::" のような2文字なら + 2("::".length() と書くと変更に強くなります)です。

区切り文字が見つからないとき

indexOf は、見つからないと -1 を返します。その値をそのまま substring に渡すと、書き方によって結果が分かれます。

public class SubstringNotFound {
    public static void main(String[] args) {
        String noAt = "taro.example.com";
        int at = noAt.indexOf('@');
        System.out.println("indexOf = " + at);

        // 「後ろ」を取る書き方は、エラーにならず全体が返る(-1 + 1 = 0 のため)
        System.out.println(noAt.substring(at + 1));

        // 「前」を取る書き方は、終了位置が -1 になり例外になる
        try {
            System.out.println(noAt.substring(0, at));
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println(e.getClass().getName() + ": " + e.getMessage());
        }

        // 直した書き方:見つかったかどうかを先に確かめる
        String local = (at >= 0) ? noAt.substring(0, at) : noAt;
        String domain = (at >= 0) ? noAt.substring(at + 1) : "";
        System.out.println("local=[" + local + "] domain=[" + domain + "]");
    }
}
indexOf = -1
taro.example.com
java.lang.StringIndexOutOfBoundsException: Range [0, -1) out of bounds for length 16
local=[taro.example.com] domain=[]

「後ろ」を取る書き方は、エラーにならないぶん気づきにくい動きをします。indexOf の結果は >= 0 かどうかを確かめてから使うと、どちらの場合も意図どおりに扱えます。

StringIndexOutOfBoundsExceptionを3パターンで再現する

Java SE 21 の API ドキュメントでは、2引数の substring が StringIndexOutOfBoundsException を投げる条件を「beginIndex が負」「endIndex がこの String の長さより大きい」「beginIndex が endIndex より大きい」の3つとしています。それぞれを実際に起こしてみます。

public class SubstringErrors {
    interface Action { String run(); }

    static void test(String label, Action action) {
        try {
            System.out.println(label + " -> " + action.run());
        } catch (RuntimeException e) {
            System.out.println(label + " -> " + e.getClass().getName() + ": " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        final String s = "Hello, World"; // 長さ 12

        // パターン1:begin が負
        test("s.substring(-1, 5)", () -> s.substring(-1, 5));
        // パターン2:end が length 超え
        test("s.substring(0, 13)", () -> s.substring(0, 13));
        // パターン3:begin > end
        test("s.substring(8, 3) ", () -> s.substring(8, 3));

        // 引数1つの形
        test("s.substring(-1)   ", () -> s.substring(-1));
        test("s.substring(13)   ", () -> s.substring(13));

        // よくある形
        test("\"abc\".substring(0, 10)", () -> "abc".substring(0, 10));
        test("\"\".substring(0, 1)    ", () -> "".substring(0, 1));
        test("\"abc\" の末尾5文字     ", () -> "abc".substring("abc".length() - 5));
    }
}

Java 21.0.10 での実行結果です。

s.substring(-1, 5) -> java.lang.StringIndexOutOfBoundsException: Range [-1, 5) out of bounds for length 12
s.substring(0, 13) -> java.lang.StringIndexOutOfBoundsException: Range [0, 13) out of bounds for length 12
s.substring(8, 3)  -> java.lang.StringIndexOutOfBoundsException: Range [8, 3) out of bounds for length 12
s.substring(-1)    -> java.lang.StringIndexOutOfBoundsException: Range [-1, 12) out of bounds for length 12
s.substring(13)    -> java.lang.StringIndexOutOfBoundsException: Range [13, 12) out of bounds for length 12
"abc".substring(0, 10) -> java.lang.StringIndexOutOfBoundsException: Range [0, 10) out of bounds for length 3
"".substring(0, 1)     -> java.lang.StringIndexOutOfBoundsException: Range [0, 1) out of bounds for length 0
"abc" の末尾5文字      -> java.lang.StringIndexOutOfBoundsException: Range [-2, 3) out of bounds for length 3

Java 21のメッセージの読み方

Range [begin, end) out of bounds for length N は、「begin から end の手前までを求められたが、長さ N の範囲に収まらない」という意味です。角かっこ [ は「その値を含む」、丸かっこ ) は「その値を含まない」を表します。

メッセージ 読み取れること 見直す場所
Range [-1, 5) out of bounds for length 12 開始位置が負 indexOf の戻り値が -1、引き算の結果が負
Range [0, 13) out of bounds for length 12 終了位置が長さより大きい 固定の文字数で切っている、長さの確認がない
Range [8, 3) out of bounds for length 12 開始が終了より後ろ 引数の順番、2つの indexOf の前後関係
Range [-2, 3) out of bounds for length 3 末尾 N 文字で N が長さより大きい length() - n が負になっている
Range [0, 1) out of bounds for length 0 空文字から切り出そうとした 入力が空のときの分岐

引数1つの substring(13) は、内部で「13 から長さ 12 まで」として扱われるため、Range [13, 12) と表示されます。

バージョンごとのエラーメッセージ比較

同じ例外でも、メッセージの文面は Java のバージョンで異なります。 検索するときやログを読むときの手がかりになるよう、4つのバージョンで同じクラスを実行した結果を並べます(クラス名 java.lang.StringIndexOutOfBoundsException: に続く部分のみ)。

呼び出し Java 8(1.8.0_504) Java 11(11.0.32.1) Java 17(17.0.20.1) Java 21(21.0.10)
s.substring(-1, 5) String index out of range: -1 begin -1, end 5, length 12 begin -1, end 5, length 12 Range [-1, 5) out of bounds for length 12
s.substring(0, 13) String index out of range: 13 begin 0, end 13, length 12 begin 0, end 13, length 12 Range [0, 13) out of bounds for length 12
s.substring(8, 3) String index out of range: -5 begin 8, end 3, length 12 begin 8, end 3, length 12 Range [8, 3) out of bounds for length 12
s.substring(-1) String index out of range: -1 String index out of range: -1 begin -1, end 12, length 12 Range [-1, 12) out of bounds for length 12
s.substring(13) String index out of range: -1 String index out of range: -1 begin 13, end 12, length 12 Range [13, 12) out of bounds for length 12

読み方のポイントです。

  • Java 8 の String index out of range: -5 は、位置ではなく「終了 − 開始」の値(3 − 8 = -5)が表示されています。substring(13) の -1 も「長さ − 開始」(12 − 13)です。
  • Java 11 以降(2引数の形)は begin・end・length がそのまま表示されるので、どの値が範囲外かをすぐ読み取れます。
  • どのバージョンでも、例外が投げられる条件と正常時の結果は同じでした。違うのはメッセージの文面だけです。

nullに対して呼ぶと別の例外になる

変数が null のときは、範囲外エラーではなく NullPointerException になります。

public class SubstringNull {
    public static void main(String[] args) {
        String name = null;
        try {
            System.out.println(name.substring(0, 1));
        } catch (NullPointerException e) {
            System.out.println(e.getClass().getName() + ": " + e.getMessage());
        }
    }
}
java.lang.NullPointerException: Cannot invoke "String.substring(int, int)" because "<local1>" is null

Java 21 では、どの変数が null だったかまでメッセージに含まれます(<local1> は、単一ファイル実行でローカル変数名の情報がないときの表示です。javac -g でコンパイルすると変数名で表示されます)。

安全に切り出すユーティリティ Math.minとnullチェック

「文字数が足りないときは、あるぶんだけ返してほしい」という場面は多くあります。位置を Math.min と Math.max で 0〜長さの範囲に収めてから substring を呼ぶと、範囲外エラーが起きなくなります。次のクラスはそのままコピーして使えます。

public class StringCut {

    /** begin〜end を 0〜長さの範囲に収めて切り出す。null は空文字として扱う。 */
    public static String safeSubstring(String s, int begin, int end) {
        if (s == null) {
            return "";
        }
        int len = s.length();
        int b = Math.max(0, Math.min(begin, len));
        int e = Math.max(b, Math.min(end, len));
        return s.substring(b, e);
    }

    /** 先頭 n 文字。n が長さより大きいときは全体を返す。 */
    public static String left(String s, int n) {
        if (s == null || n <= 0) {
            return "";
        }
        return s.substring(0, Math.min(n, s.length()));
    }

    /** 末尾 n 文字。n が長さより大きいときは全体を返す。 */
    public static String right(String s, int n) {
        if (s == null || n <= 0) {
            return "";
        }
        return s.substring(Math.max(0, s.length() - n));
    }

    public static void main(String[] args) {
        System.out.println("[" + safeSubstring("abc", 0, 10) + "]");
        System.out.println("[" + safeSubstring("abc", -2, 2) + "]");
        System.out.println("[" + safeSubstring("abc", 5, 2) + "]");
        System.out.println("[" + safeSubstring(null, 0, 3) + "]");
        System.out.println("[" + left("Hello", 2) + "]");
        System.out.println("[" + left("abc", 10) + "]");
        System.out.println("[" + right("Hello", 2) + "]");
        System.out.println("[" + right("abc", 10) + "]");
        System.out.println("[" + right(null, 2) + "]");
    }
}
[abc]
[ab]
[]
[]
[He]
[abc]
[lo]
[abc]
[]
行 何をしているか
Math.min(begin, len) 開始位置が長さを超えないようにする
Math.max(0, ...) 開始位置が負にならないようにする
Math.max(b, Math.min(end, len)) 終了位置を長さ以下に収め、かつ開始位置より前にならないようにする
Math.max(0, s.length() - n) 末尾 N 文字で、N が長さより大きいときに 0 から切り出す
if (s == null) return ""; null を空文字として扱う(null を返す方針にしたい場合はここを変える)

直す前と後

一覧画面で、長い題名を先頭20文字に切り詰める例です。

public class TruncateTitle {
    static String truncate(String title, int max) {
        if (title == null) {
            return "";
        }
        if (title.length() <= max) {
            return title;
        }
        return title.substring(0, max) + "…";
    }

    public static void main(String[] args) {
        String shortTitle = "週次ミーティングの議事録";
        String longTitle = "備品管理システムの操作手順と問い合わせ先の一覧(2026年10月版)";

        // 直す前:20文字より短いと例外になる
        try {
            System.out.println(shortTitle.substring(0, 20));
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println("直す前: " + e.getMessage());
        }

        // 直した後:長さを確かめてから切る
        System.out.println("直した後: " + truncate(shortTitle, 20));
        System.out.println("直した後: " + truncate(longTitle, 20));
    }
}
直す前: Range [0, 20) out of bounds for length 12
直した後: 週次ミーティングの議事録
直した後: 備品管理システムの操作手順と問い合わせ先…

「固定の文字数で切る処理」には、長さの確認を必ずセットにすると覚えておくと、短い入力が来ても止まらないコードになります。

nullを「空文字にする」か「nullのまま返す」か

方針 書き方 向いている場面
空文字を返す if (s == null) return ""; 画面表示や文字列連結にそのまま使う
null のまま返す if (s == null) return null; 「値がない」ことを呼び出し側で区別したい
例外にする Objects.requireNonNull(s, "s") null が来ないはずの場所で、早めに気づきたい

どれを選んでも構いませんが、同じアプリケーションの中で方針をそろえると、呼び出す側が迷いません。

絵文字(サロゲートペア)が壊れる例と対処

Java の String は、内部で文字を UTF-16 の char(16ビット)の並びとして持っています。多くの文字は char 1つですが、絵文字や一部の漢字は char 2つ(サロゲートペア)で1文字を表します。length() と substring の位置は char の数で数えるため、ペアの真ん中で切ると文字が壊れます。

import java.nio.charset.StandardCharsets;

public class SurrogateDemo {
    static String hex(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            sb.append(String.format("%04X ", (int) c));
        }
        return sb.toString().trim();
    }

    public static void main(String[] args) {
        String s = "寿司🍣が好き😊";
        System.out.println("length()       = " + s.length());
        System.out.println("codePointCount = " + s.codePointCount(0, s.length()));

        // 見た目の「先頭3文字」のつもりで substring(0, 3)
        String cut = s.substring(0, 3);
        System.out.println("char の中身    = " + hex(cut));
        System.out.println("末尾が上位サロゲートだけ = " + Character.isHighSurrogate(cut.charAt(2)));
        System.out.println("UTF-8 のバイト数 = " + cut.getBytes(StandardCharsets.UTF_8).length);

        // 絵文字まで含めるには 4 が必要
        System.out.println(s.substring(0, 4));
    }
}
length()       = 9
codePointCount = 7
char の中身    = 5BFF 53F8 D83C
末尾が上位サロゲートだけ = true
UTF-8 のバイト数 = 7
寿司🍣

見た目は7文字ですが、length() は 9 です。substring(0, 3) の結果は「寿」「司」と、🍣 の前半(D83C)だけになります。この半分だけの char は単独では文字として成り立たないため、UTF-8 に変換すると ? 1バイトに置き換わりました(漢字2文字で6バイト+1バイト=7バイト)。画面やファイルでは「?」や「□」のように表示されます。

offsetByCodePointsで文字単位の位置を求める

s.offsetByCodePoints(起点, 文字数) は、「起点から指定した文字数(コードポイント数)だけ進んだ char の位置」を返します。 この値を substring に渡せば、ペアの途中で切れません。

public class CodePointCut {

    /** コードポイント単位で先頭 n 文字を返す。 */
    public static String leftByCodePoint(String s, int n) {
        if (s == null || n <= 0) {
            return "";
        }
        if (n >= s.codePointCount(0, s.length())) {
            return s;
        }
        return s.substring(0, s.offsetByCodePoints(0, n));
    }

    /** コードポイント単位で末尾 n 文字を返す。 */
    public static String rightByCodePoint(String s, int n) {
        if (s == null || n <= 0) {
            return "";
        }
        if (n >= s.codePointCount(0, s.length())) {
            return s;
        }
        return s.substring(s.offsetByCodePoints(s.length(), -n));
    }

    public static void main(String[] args) {
        String s = "寿司🍣が好き😊";
        System.out.println("offsetByCodePoints(0, 3) = " + s.offsetByCodePoints(0, 3));
        System.out.println(leftByCodePoint(s, 3));
        System.out.println(leftByCodePoint(s, 100));
        System.out.println(rightByCodePoint(s, 1));
        System.out.println(rightByCodePoint(s, 3));

        String name = "𠮷野";
        System.out.println(name.length() + " / " + leftByCodePoint(name, 1));

        try {
            s.offsetByCodePoints(0, 99);
        } catch (IndexOutOfBoundsException e) {
            System.out.println(e.getClass().getName());
        }
    }
}
offsetByCodePoints(0, 3) = 4
寿司🍣
寿司🍣が好き😊
😊
好き😊
3 / 𠮷
java.lang.IndexOutOfBoundsException
  • 「3文字進んだ位置」は 4 と求まり、substring(0, 4) で絵文字まで含めて切り出せました。
  • 末尾から数えるときは、起点を s.length()、文字数を負の値にします。
  • 「𠮷」のような漢字もサロゲートペアです。"𠮷野".length() は 3 になります。
  • 文字数が足りないと offsetByCodePoints 自体が IndexOutOfBoundsException を投げるため、先に codePointCount で全体の文字数と比べています。

結合された絵文字はさらに別の単位

家族の絵文字のように、複数の絵文字を「ゼロ幅接合子」でつないで1つに見せる文字もあります。

import java.text.BreakIterator;

public class GraphemeDemo {
    public static void main(String[] args) {
        String family = "👨‍👩‍👧";
        System.out.println("length()       = " + family.length());
        System.out.println("codePointCount = " + family.codePointCount(0, family.length()));

        BreakIterator it = BreakIterator.getCharacterInstance();
        it.setText(family);
        int count = 0;
        while (it.next() != BreakIterator.DONE) {
            count++;
        }
        System.out.println("BreakIterator  = " + count);
    }
}
length()       = 8
codePointCount = 5
BreakIterator  = 1

見た目は1文字でも、char は 8 個、コードポイントは 5 個です。Java 21 の java.text.BreakIterator は、これを1つのまとまりとして数えました。見た目どおりの単位で切りたいときは、BreakIterator で境界の位置を求めてから substring に渡す方法があります。

数え方 取得方法 「寿司🍣が好き😊」 向いている場面
char の数 s.length() 9 英数字や通常の日本語だけを扱う
コードポイント数 s.codePointCount(0, s.length()) 7 絵文字や「𠮷」を含む入力を文字数で切る
見た目の文字のまとまり BreakIterator.getCharacterInstance() (本文の例は家族の絵文字で 1) 結合された絵文字まで見た目どおりに扱う

利用者が自由に入力する文字列(表示名、コメントなど)を文字数で切り詰めるときは、コードポイント単位の leftByCodePoint を使っておくと、絵文字が混ざっても表示が崩れにくくなります。

Java 8〜21での動作確認

バージョンをまたいで使えるかを確かめるため、基本の切り出しと offsetByCodePoints を含むクラスを javac --release 8 でコンパイルし、4つの実行環境で動かしました。

確認した内容 Java 8 Java 11 Java 17 Java 21
"Hello, World".substring(7) → World 同じ 同じ 同じ 同じ
substring(12) → 空文字 同じ 同じ 同じ 同じ
3パターンで StringIndexOutOfBoundsException 発生 発生 発生 発生
「寿司🍣が好き😊」の length() = 9、コードポイント数 = 7 同じ 同じ 同じ 同じ
offsetByCodePoints(0, 3) = 4 同じ 同じ 同じ 同じ
エラーメッセージの文面 String index out of range: N 2引数は begin b, end e, length n begin b, end e, length n Range [b, e) out of bounds for length n

substring の結果と例外の条件は Java 8 から 21 まで同じで、変わったのはメッセージの文面だけです。メッセージの文字列を条件分岐やテストの期待値に使っている場合は、バージョンを上げたときに文面が変わる点に気をつけます。例外の種類(クラス)で判定する書き方にしておくと、どの版でも同じように動きます。Java 22 以降は未実行です。

substring以外の切り出し方との使い分け

やりたいこと 向いている方法 例
位置がわかっている部分を取る substring s.substring(0, 3)
区切り文字で全部に分ける split "a,b,c".split(",")
1文字だけ取る charAt s.charAt(0)
前後の空白を除く strip(Java 11 以降)、trim s.strip()
パターンに合う部分を取る 正規表現(Pattern と Matcher) 数字の並びだけを取り出す など
変更できる文字列から取る StringBuilder.substring sb.substring(1, 3)

StringBuilder にも同じ名前のメソッドがあり、範囲外のときの動きも同じでした。

public class BuilderSubstring {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("Hello");
        System.out.println(sb.substring(1, 3));
        try {
            System.out.println(sb.substring(1, 9));
        } catch (StringIndexOutOfBoundsException e) {
            System.out.println(e.getMessage());
        }
        System.out.println("  abc  ".strip().substring(0, 2));
    }
}
el
Range [1, 9) out of bounds for length 5
ab

範囲外エラーが出たときのチェックリスト

確認すること 見るポイント 直し方
文字列の長さは足りているか メッセージの length の値 Math.min(n, s.length()) で上限を決める
末尾 N 文字で N が長さを超えていないか begin が負になっている Math.max(0, s.length() - n)
indexOf が -1 を返していないか begin か end が -1 if (idx >= 0) で分岐する
開始と終了が逆になっていないか begin が end より大きい 引数の順番と、2つの位置の前後関係を確かめる
入力が空文字ではないか length 0 s.isEmpty() のときの戻り値を決める
変数が null ではないか NullPointerException 先に null を判定する
終了位置を「文字数」と取り違えていないか 結果が短い、または範囲外 終了位置は 開始位置 + 文字数
絵文字が混ざっていないか 末尾が「?」になる offsetByCodePoints で位置を求める

よくある質問

Q. substring の第2引数は「文字数」ですか、「位置」ですか? A. 位置(終了位置)です。その位置の文字は結果に含まれません。開始位置から N 文字を取りたいときは s.substring(begin, begin + n) と書きます。取り出される文字数は「終了位置 − 開始位置」です。

Q. 末尾から N 文字を取り出すにはどう書きますか? A. s.substring(s.length() - n) です。文字列が N 文字より短い可能性があるときは s.substring(Math.max(0, s.length() - n)) とすると、範囲外エラーにならず全体が返ります。

Q. StringIndexOutOfBoundsException が出たら、まずどこを見ればよいですか? A. メッセージに含まれる数値です。Java 21 なら Range [begin, end) out of bounds for length N の3つの値を見て、begin が負か、end が N より大きいか、begin が end より大きいかを確かめます。原因の多くは、文字列が想定より短い、または indexOf が -1 を返している、のどちらかです。

Q. 全角文字や日本語でも位置の数え方は同じですか? A. ひらがな・カタカナ・多くの漢字は1文字が char 1つなので、半角文字と同じように数えられます。絵文字や「𠮷」などは char 2つで1文字になるため、これらが混ざる入力では offsetByCodePoints で位置を求めてから切り出します。

まとめ

  • substring(begin, end) は begin から end の手前までを返します。位置は 0 始まりで、文字数は end - begin です。
  • 末尾 N 文字は s.substring(s.length() - n)、区切り文字の後は s.substring(s.indexOf(区切り) + 区切りの長さ) で取り出せます。
  • 範囲外エラーの条件は「begin が負」「end が length 超え」「begin が end より大きい」の3つです。Java 21 では Range [begin, end) out of bounds for length N と表示されます。
  • エラーメッセージの文面は Java 8・11・17・21 で異なり、結果と例外の条件は同じでした。判定には例外のクラスを使います。
  • Math.min と Math.max で位置を 0〜長さに収め、null を先に判定すると、短い入力や空の入力でも止まらないコードになります。
  • indexOf の戻り値は -1 の場合があるので、>= 0 を確かめてから substring に渡します。
  • 絵文字を含む文字列は offsetByCodePoints で位置を求めて切り出すと、文字が壊れません。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java substringの使い方 末尾切り出しと範囲外エラーの直し方」

株式会社RJCでは、Javaの文字列操作や例外の読み解き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?