- JAVA
Java substringの使い方 末尾切り出しと範囲外エラーの直し方
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期連続の黒字決算。