Java

substring を使わない方が良い3つの理由 [Java]

この記事で解決できる問題

  • substring に関する3つの問題点が分かる
  • substring に似た機能を持つクラスのサンプルコードが欲しい
  • バグを少なくする文字列操作のコツが知りたい

この記事では、Javaにおけるsubstringの闇を暴きます。

非常に罠が多いにも関わらず、初心者用の解説サイトでも出現頻度が高い有名なメソッドです。
しかし、この難しいメソッドの使い方をわざわざ覚える前に、もっと便利なクラスを使えば様々なトラブルを未然に防ぐ事ができるんです!

ゴイチ

私は substring を全力で避けるようにしています。
そもそも、何回見ても使い方が覚えられません(笑

もちろん闇を暴くだけでなく、substringを使わないで済むように代わりのメソッドについても紹介しています。
今後のJava エンジニアライフにとって必ずやプラスになる内容なので、ぜひ最後まで読んで substringさんとは金輪際お別れして下さい!

substring の問題点

まず説明のために、 String#substringのリファレンスを載せます。

substring

public String substring(
  int beginIndex,
  int endIndex
)

この文字列の部分文字列である文字列を返します。部分文字列は、指定されたbeginIndexから始まり、インデックスendIndex - 1にある文字までです。したがって、部分文字列の長さはendIndex-beginIndexになります。


"hamburger".substring(4, 8) returns "urge"
"smiles".substring(1, 5) returns "mile"

パラメータ:
beginIndex - 開始インデックス(この値を含む)。
endIndex - 終了インデックス(この値を含まない)

戻り値:
指定された部分文字列。

例外:
IndexOutOfBoundsException - beginIndexが負であるか、endIndexがこのStringオブジェクトの長さより大きいか、あるいはbeginIndexがendIndexより大きい場合。

https://docs.oracle.com/javase/jp/8/docs/api/java/lang/String.html#substring-int-int-

問題点 その1 インデックス指定

beginIndex で指定したインデックスの文字は含まれる。
endIndex で指定したインデックスの文字は含まれない

この仕様が何回見ても分かりにくい!
インデックスで指定している範囲がどこからどこまでなのかを注意深く見なければならないし、ソースコードをパッと見た時に正しいコードなのかが全く分からない。

ゴイチ

もはや、書いた人を信用してお祈りするしかない(笑

いや、そこはちゃんと見ろよ!

ツッコミを入れる人

問題点 その2 例外の発生

endIndex がこのStringオブジェクトの長さより大きい場合、例外が発生します。
致命的な問題ではありませんが、少し不親切な仕様だと思います。

ゴイチ

例外ではなく、最後の文字までを切り出してくれた方が使いやすそう

問題点 その3 Lengthと間違う

第二引数の endIndex length と勘違いしやすい。

"foobarfizzbuzz1234".substring(6, 10)

の出力が、"fizz" になるのか "fizzbuzz12" になるのか、substringの仕様をちゃんと理解していないと分かりません。

ゴイチ

lengthを指定するタイプのメソッドも多いので混乱しますね

つまり何が言いたいかというと、勘違いを生みやすいこれらの仕様がバグの温床になりやすいということです。

便利な文字列操作クラス(StringUtils)の紹介

Apache Commons Langに、StringUtils という便利なクラスがあります。

英語のリファレンスはこちらにあります。
https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html

ゴイチ

今でもバージョンアップされていて、知らないうちに便利なメソッドが増えていますね

以下で、substring の代わりに使うことができるStringUtilsのメソッドについて紹介します。

基本的にこれらのメソッドを組み合わせて、substring と同じ動作をするようにコードを書きます。
インデックス(添字)に悩まされることがなくなるため、可読性が非常に良くなります

Left / Right

left

public static String left(String str, int len)

String の左端 len 文字を取得する。

  • len文字に満たない場合、あるいは文字列がnullの場合、例外を発生させずに文字列が返される。
  • len が負の場合、空の文字列が返される。

right

public static String right(String str, int len)

String の右端 len 文字を取得する。

  • len文字に満たない場合、あるいは文字列がnullの場合は、例外を発生させずに文字列が返される。
  • len が負の場合、空の文字列が返される。

SubstringBefore / SubstringAfter / SubstringBetween

substringBefore

public static String substringBefore(String str, String separator)

セパレータが最初に出現する前の部分文字列を取得する。セパレータは返さない。

  • null文字列を入力すると、nullが返される。
  • 空文字列("")入力は、空文字列を返す。
  • セパレータがnullの場合、入力された文字列が返される。
  • 何も見つからなかった場合は、入力された文字列が返される。

substringAfter

public static String substringAfter(String str, String separator)

セパレータが最初に出現した後の部分文字列を取得する。セパレータは返さない。

  • null文字列を入力すると、nullが返される。
  • 空文字列("")入力は、空文字列を返す。
  • nullセパレータは、入力文字列がnullでない場合、空の文字列を返します。

substringBetween

public static String substringBetween(String str, String open, String close)

2つのStringの間に入れ子になっている文字列を取得します。最初にマッチしたものだけが返されます。

  • 入力文字列がnullの場合は、nullを返します。
  • nullの open/close は、null(マッチしない)を返します。
  • 空文字列("")の open/close は、空の文字列を返します。

SubstringBeforeLast / SubstringAfterLast

substringBeforeLast

public static String substringBeforeLast(String str, String separator)

セパレータが最後に現れる前の部分文字列を取得する。セパレータは返さない。

  • null文字列を入力すると、nullが返される。
  • 空文字列("")入力は、空文字列を返す。
  • セパレータが空またはnullの場合、入力された文字列が返される。
  • 何も見つからなかった場合は、入力された文字列が返される。

substringAfterLast

public static String substringAfterLast(String str, String separator)

セパレータが最後に出現した後の部分文字列を取得する。セパレータは返さない。

  • null文字列を入力すると、nullが返される。
  • 空文字列("")入力は、空文字列を返す。
  • 空またはnullセパレータは、入力文字列がnullでない場合、空の文字列を返します。
  • 何も見つからなかった場合は、空文字列が返される。

Split

substringAfterLast

public static String[] split(String str, String separatorChars)

指定されたテキストを、指定されたセパレータで配列に分割します。これは StringTokenizer を使用する代替方法です。

セパレータは、返される String 配列には含まれません。隣接するセパレータは、ひとつのセパレータとして扱われます。分割をより詳細に制御するには、StrTokenizer クラスを使用します。

  • null の入力文字列は null を返します。
  • null の separatorChars は、空白で分割されます。

文字列操作の具体例

この章では、StringUtils のメソッドを使ってどのように substring と同じ処理を実現するかを具体的に紹介します。

String fileName = "/usr/local/temp/fileName.txt";
String uri = "https://4engineer.net/java/nomore-substring";

// 拡張子(txt)のみを取得
StringUtils.right(fileName, 3);
StringUtils.split(fileName, '.')[1];
StringUtils.substringAfterLast(fileName, '.');

// ファイル名(fileName)のみを取得
StringUtils.substringAfterLast(StringUtils.split(fileName, '.')[0], "/");
StringUtils.substringAfterLast(StringUtils.substringBeforeLast(fileName, "."), "/");
StringUtils.substringBefore(StringUtils.substringAfterLast(fileName, "/"), ".");

// ドメイン名(4engineer.net)のみを取得
StringUtils.substringBefore(StringUtils.split(uri, "://")[1], "/");
StringUtils.substringBefore(StringUtils.substringAfter(uri, "://"), "/");
StringUtils.substringBetween(uri, "://", "/");

ここがポイント

例として3つずつ書いてみましたが、どれも3番目の書き方が一番きれいでバグが少なくなると思います。
組み合わせ方で色々な書き方ができますが、即値のマジックナンバーやインデックス指定が無い書き方の方がベターです。

このように、StringUtilsのメソッドを組み合わせることで、大抵の場合 indexOf + substring を置き換えることができます。

まとめ

それでは、最後にここまでの情報をまとめます。

要点まとめ

  • Stringクラスの substring メソッドは仕様を勘違いしやすい
  • StringUtils という文字列操作用の便利なクラスがある
  • StringUtils の提供するメソッドを組み合わせることで substring 相当の処理ができる

文字列操作をする際に、インデックス値を直接扱わないことが可読性の高いコードを書くことに繋がります。
substring と同じ理由で indexOf メソッドも同様の問題を抱えていますので、この2つをなるべく使わないようにすることがバグを少なくするコツだと思います。

ゴイチ

substring を使ったら負けかなと思ってる(AA略

それでは、よい Javaライフを!

この記事は役に立ちましたか?

  • この記事を書いた人
アバター画像

ゴイチ

ソフトウェアエンジニア歴20年。 C/C++、Java、C#、Kotlinが得意で、組込系からAndroidアプリ、大規模なWebサービスなど幅広いプログラミング経験があります。 現在は某SNSの会社でWebエンジニアをしています。

-Java
-