Javaについて徹底解説!

みんな実は困っていた? 半角・全角空白をJavaではどうtrimすべきか

大石 英人

大石 英人

開発エンジニア/Java20年/Java GOLD/リーダー/ボールド歴2年

String.trimは、文字列の前後にある空白文字を削除するメソッドです。実際のプログラムでも、かなり頻繁に使うものです。

ただ、ここで「空白」と言った時に、アジアの片隅に生きる日本人の立場の弱さを感じる時があります。例えば、いわゆる全角空白も削除したいですよね。でも、残念ながらString.trimは全角空白を削除してはくれません。これは何とかしたいです。

そして、プログラムで削除したい文字は空白だけとは限りません。“”といったクォーテーションを削除したい時もあります。それに、SQLには前や後ろからだけ文字を削除してくれるLTRIMRTRIMがあるのに、Javaにはなぜないのだろうと思っている方も多いでしょう。

この記事では、String.trimを足掛かりに、文字列から余分な文字を削除する方法を、プログラムの実例をたくさん交えながらお伝えします。

※この記事のサンプルは、Java 11の環境で動作確認しています。


1.【基本】文字列の空白文字を削除するtrim

まずは基本から行きましょう。文字列の前後にある「空白」を削除するには、String.trimを使います。戻り値は、前後から空白が削除された文字列です。

// Java 11のJavadocより抜粋
public String trim()
戻り値:
	値がこの文字列で、先頭と末尾のすべてのスペースが削除されているか、先頭または末尾のスペースがない場合は文字列。
String str = "  A B C  "; // 前後に空白(' ')が付いている
String trimmed = str.trim();
System.out.println(trimmed); // → "ABC"

ですが、trimにはいくつかの困ったところがあります。それを一緒に見ていきましょう。

1-1.String.trimは片側の空白だけを削除できない

trimは常に文字列の両側から空白を削除します。残念ながら、片側の空白だけを削除はできません。

文章やプログラムでは、文字列先頭の空白やタブへは意味を与えていることがあるので、消したくないケースがあります。例えば、タブをインデントに使っていたりする場合は、後ろの空白だけ消したいとかtrimはそういう用途には使えません。

1-2.String.trimは空白扱いする文字が固定されている

trimが空白扱いする文字は固定されていて、変更できません。String.trimJavadocにはこう書いてあります(Java 11の日本語版/英語版Javadoc)

値がこの文字列で、先頭と末尾のすべてのスペースが削除され、コード・ポイントが‘U+0020’ (空白文字)以下の文字でスペースが定義されている文字列を返します。

Returns a string whose value is this string, with all leading and trailing space removed, where space is defined as any character whose codepoint is less than or equal to ‘U+0020’ (the space character).

つまり、コードポイントが空白以下の文字を、文字列の両端から削除してくれます(※コードポイントについては後述します)。対象の文字は、タブや改行、色々な制御文字など、普通は印字しない文字です。

これはJava 1.0からのtrimの仕様です(Javadocに明確に書かれているのは、恐らく1.1から)。動作を変えると過去に作ったプログラムが動かなくなる可能性があるので、きっとずっとこのままでしょう。

1-3.String.trimは全角空白を削除してくれない

英語環境ならこのtrimでまあ充分なのですが、そう…trimはいわゆる全角空白を空白として認識してくれず、削除しません。必要なら自分で処理を作る必要があります。日本人にとっては、少し使いづらいですよね。

String str = "   A B C   "; // ABCの前後に全角空白あり
String trimmed = str.trim();
System.out.println(trimmed); // → "  A B C  "、trimは全角空白を削除してくれない…

2.全角空白も削除するstrip/stripLeading/stripTrailingJava 11~】

Java 11でString.stripが追加されました。stripは、全角空白も含む広い意味での「空白」を削除してくれます。stripの仲間として、前からだけ削除するstripLeading、後ろからだけ削除するstripTrailingもあります。

// Java 11のJavadocより抜粋
public String strip()
戻り値:
	値がこの文字列で、先頭と末尾の空白がすべて削除されている文字列
// Java 11のJavadocより抜粋
public String stripLeading()
戻り値:
	値がこの文字列で、先頭に空白がすべて削除されている文字列
// Java 11のJavadocより抜粋
public String stripTrailing()
戻り値:
	値がこの文字列で、後続の空白がすべて削除されている文字列
String str = "   A B C   "; // ABCの前後に全角/半角空白あり
String stripped1 = str.strip();
String stripped2 = str.stripLeading();
String stripped3 = str.stripTrailing();
System.out.println(stripped1); // → "A B C"、全角空白も削除された!!
System.out.println(stripped2); // → "A B C   "、文字列の前の空白だけ削除された!!
System.out.println(stripped3); // → "   A B C"、文字列の後ろの空白だけ削除された!!

2-1.stripが削除する空白はCharacter.isWhitespaceが判断する

stripが削除する空白は、Character.isWhitespacetrueを戻す文字です。stripJavadocの記述は以下のとおりで、「white space」の部分にはCharacter.isWhitespaceへのハイパーリンクがあります。

値がこの文字列であり、先頭と末尾のすべてのwhite spaceが削除されている文字列を返します。

Returns a string whose value is this string, with all leading and trailing white space removed.

そして、Character.isWhitespaceJavadocでの記述は以下のとおりです。現実世界で空白扱いされる文字は、全角空白を含め、大体はその範囲内です。実際にどの文字が対象かは別の章で簡単にお伝えします。

指定された文字(Unicodeコード・ポイント)Javaの基準に従った空白かどうかを判定します。 次の基準のどれかを満たす場合にだけ、Javaの空白文字になります。

Determines if the specified character (Unicode code point) is white space according to Java. A character is a Java whitespace character if and only if it satisfies one of the following criteria:

 


3.欲しいtrimを自分で作る

Strin.trimの不便さや、Java 11でのstripとその仲間たちが便利なのは分かりました。でも、Java 11ではない環境ではどうすればいいのでしょうか。

やはり、欲しいものは自分で作るか、外部ライブラリを使うことになります。この章では、必要な機能を持つtrimを自分自身で作る方法を探ってみます。

3-1.【お手軽】replaceFirst/replaceAllによるtrim

一番簡単なのは、文字列を置換するメソッドString.replaceFirstreplaceAllを使うものです。replaceFirst/replaceAllで置換する文字を指定するには、正規表現と呼ばれるパターンが使えます。

例えば、全角・半角空白両対応のtrimをしたければ、正規表現として \h を指定するだけです。

String str = "   A B C   "; // ABCの前後に全角/半角空白あり
String replaced1 = str.replaceFirst("^[\\h]+", "").replaceFirst("[\\h]+$", "");
String replaced2 = str.replaceFirst("^[\\h]+", "");
String replaced3 = str.replaceFirst("[\\h]+$", "");
System.out.println(replaced1); // → "A B C"
System.out.println(replaced2); // → "A B C   "
System.out.println(replaced3); // → "   A B C"

このパターンでは、先頭(^)の後と末尾($)の前で空白扱いしたい文字があれば、それを“”に置換しているだけです。前だけ、後ろだけをtrimしたい場合は、対応するreplaceFirstだけを呼びましょう。

これらをメソッドにしてみると、こんな感じです。nullと文字数のチェックだけ追加した感じですね。

String strip(String str) {
	return stripTrailing(stripLeading(str));
}

String stripLeading(String str) {
	if (str == null || str.isEmpty()) {
		return str;
	}

	return str.replaceFirst("^[\\h]+", "");
}

String stripTrailing(String str) {
	if (str == null || str.isEmpty()) {
		return str;
	}

	return str.replaceFirst("[\\h]+$", "");
}

なお、この例ではたまたま空白を使っていますが、置換したい文字は正規表現で自由に指定できます。これがreplaceFirst/replaceAllの便利な所です。でも、対象とする文字の数が増えてくると、なかなか面倒になってきます。

String str = "X Y  ZA B CZ X  Y"; // ABCの前後に全角/半角空白あり
String replaced = str.replaceFirst("^[\\hXYZ]+", "").replaceFirst("[\\hXYZ]+$", "");
System.out.println(replaced); // → "A B C"

3-1-1.【参考】Javaの正規表現の定義済み文字クラス

先程の例では、空白を指定するのに文字クラス \h を使いました。同じようなものがいくつかありますので、簡単に紹介します。詳しくはPatternJavadocを参照してください。

Pattern (Java SE 11 & JDK 11)

https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/Pattern.html

文字クラス説明
.任意の文字行末記号とマッチする場合もあります。
\d数字0~9までの数字です。
\D数字以外\dを否定したものです。
\h水平方向の空白文字タブや改行など。全角空白もこれに含まれます(!)
\H水平方向の空白文字以外\hを否定したものです。
\s空白文字空白、改行、タブなどです。
\S空白文字以外\sを否定したものです。
\w単語文字大小のアルファベットと数字です。
\W単語文字以外\wを否定したものです。

3-2.文字でループして判断する

Stringが持つ文字でループをして、削除したい文字かを判断する処理を作るのも、地味ながら確実です。削除したい文字の判断を自分でできるので、柔軟にも作れます。

例えば、そんなメソッドを持つクラスを素直に作るなら、以下のとおりです。文字削除の判断ロジックはCharacter.isWhitespaceをそのまま使いました。これなら、今使われているどんなJavaの環境でも動くでしょう。

class Stripper {
	static String strip(String str) {
		return stripTrailing(stripLeading(str));
	}

	static String stripLeading(String str) {
		if (str == null || str.isEmpty()) {
			return str;
		}

		char[] chars = str.toCharArray();
		int firstIndex = 0;

		// 文字列の先頭から削除対象でない文字を探して…
		for (; firstIndex < chars.length; firstIndex++) {
			if (!isTarget(chars[firstIndex])) {
				break;
			}
		}

		return str.substring(firstIndex); // substringして戻す
	}

	static String stripTrailing(String str) {
		if (str == null || str.isEmpty()) {
			return str;
		}

		char[] chars = str.toCharArray();
		int lastIndex = chars.length - 1;

		// 文字列の最後から削除対象でない文字を探して…
		for (; lastIndex >= 0; lastIndex--) {
			if (!isTarget(chars[lastIndex])) {
				break;
			}
		}

		return lastIndex >= 0 ? str.substring(0, lastIndex + 1) : str; // substringして戻す
	}

	private static boolean isTarget(char c) {
		return Character.isWhitespace(c);
	}
}
String str = "   A B C   "; // ABCの前後に全角/半角空白あり
String stripped1 = Stripper.strip(str);
String stripped2 = Stripper.stripLeading(str);
String stripped3 = Stripper.stripTrailing(str);
System.out.println(stripped1); // → "A B C"、全角空白も削除された!!
System.out.println(stripped2); // → "A B C   "、文字列の前の空白だけ削除された!!
System.out.println(stripped3); // → "   A B C"、文字列の後ろの空白だけ削除された!!

なお、CharacterにはisWhitespaceの他にも、色々な文字種を確認するためのis~系メソッドがあります。これらを組み合わせてもいいでしょう。

3-2-1.【発展】もう少し汎用的に作ってみる

さきほど作ったStripperをもう少し汎用的にすると、例えば以下のようになります。このクラスを動かすにはJava 9以降が必要です。でも、String.codePointsStreamを使っている箇所を作り直せば、Java 1.5以降であれば動くはずです。

import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;

class Stripper {
	private final boolean whitespace;
	private Set<Integer> targets;

	/**
	 * Stripperを生成する。空白の削除のみとなる。
	 */
	Stripper() {
		this(new int[] {}, true);
	}

	/**
	 * Stripperを生成する。引数で指定した文字のみ削除する。
	 *
	 * @param targetStr 削除対象の文字
	 */
	Stripper(String targetStr) {
		this(targetStr, false);
	}

	/**
	 * Stripperを生成する。空白の削除と、引数で指定した文字を合わせて削除するか指定できる。
	 * @param targetStr 削除対象の文字
	 * @param hitespace 空白を削除するか true:削除する false:削除しない
	 */
	Stripper(String targetStr, boolean hitespace) {
		this(targetStr.codePoints().toArray(), hitespace);
	}

	/**
	 * Stripperを生成する。削除対象の文字はコードポイントで指定する。
	 *
	 * @param target 削除対象の文字のコードポイント
	 */
	Stripper(int[] target) {
		this(target, false);
	}

	/**
	 * Stripperを生成する。空白の削除と、引数で指定したコードポイントの文字を合わせて削除するか指定できる。
	 * @param target 削除対象の文字のコードポイント
	 * @param hitespace 空白を削除するか true:削除する false:削除しない
	 */
	Stripper(int[] target, boolean whitespace) {
		targets = Arrays.stream(target).boxed().collect(Collectors.toSet());
		this.whitespace = whitespace;
	}

	void add(String str) {
		str.codePoints().forEach(targets::add);
	}

	void add(int c) {
		targets.add(c);
	}

	void add(char c) {
		targets.add((int) c);
	}

	String strip(String str) {
		return stripTrailing(stripLeading(str));
	}

	String stripLeading(String str) {
		if (str == null || str.isEmpty()) {
			return str;
		}

		int[] codepoints = str.codePoints().toArray();
		int firstIndex = 0;

		for (; firstIndex < codepoints.length; firstIndex++) {
			if (!isTarget(codepoints[firstIndex])) {
				break;
			}
		}

		return new String(codepoints, firstIndex, codepoints.length - firstIndex);
	}

	String stripTrailing(String str) {
		if (str == null || str.isEmpty()) {
			return str;
		}

		int[] codepoints = str.codePoints().toArray();
		int lastIndex = codepoints.length - 1;

		for (; lastIndex >= 0; lastIndex--) {
			if (!isTarget(codepoints[lastIndex])) {
				break;
			}
		}

		return new String(codepoints, 0, lastIndex + 1);
	}

	private boolean isTarget(int c) {
		boolean contains = false;

		if (whitespace) {
			contains = Character.isWhitespace(c);
		}

		return !contains && !targets.isEmpty() ? targets.contains(c) : contains;
	}
}

このクラスとメソッドは、以下のような感じで使います。削除対象にできる文字についてはUnicodeのサロゲートペアも考慮していますので、例えばいわゆる「つちよし」や「はしご高」も使えます。削除する文字は、コンストラクタで指定する以外にも、インスタンスを作った後にもaddで追加できます。

String str = "Z X Y A B C Y X Z"; // ABCの前後に全角/半角空白と余分な文字あり
String stripped1 = new Stripper("XYZ", true).strip(str);
String stripped2 = new Stripper("XYZ", true).stripLeading(str);
String stripped3 = new Stripper("XYZ", true).stripTrailing(str);
System.out.println(stripped1); // → "A B C"、指定した文字と空白が削除された!!
System.out.println(stripped2); // → "A B C Y X Z"、文字列の前からだけ削除された!!
System.out.println(stripped3); // → "Z X Y A B C"、文字列の後ろからだけ削除された!!

4.【便利】外部ライブラリのtrim的なメソッドを使う

Javaには文字・文字列を扱うためのライブラリが多数あります。その中でも有名なもの、例えばApache Commons-LangGuavaには、trimをするための便利なクラス・メソッドがあります。早速いくつかを使ってみましょう。

4-1.Apache Commons-LangのStringUtilsを使う

Apache Commons-Langでtrim的な操作をするなら、StringUtils.stripなどを使います。

StringUtils (Apache Commons Lang 3.9-SNAPSHOT API)

https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html

String str = "   A B C   "; // ABCの前後に全角/半角空白あり
String stripped = StringUtils.strip(str);
System.out.println(stripped); // → "A B C"

stripは、引数が一つのものだとCharacter.isWhitespaceを使います。引数が二つstripは、削除したい文字を指定できます(サロゲートペアには対応していないようです)

String str = "XYZA B CZYX"; // ←先頭・末尾にXYZがついている
String stripped = StringUtils.strip(str, "XYZ"); // XYZをstripさせてみると…
System.out.println(stripped); // → "A B C"

これらの他にも、頭だけ、終わりだけ、nullだったら“”にする、“”だったらnullにするなど様々な種類のstripがあります。ぜひAPIを見て、使えそうなものを探してみましょう。

4-2.GuavaのCharMatcherを使う

GoogleのGuavatrim的な操作をするなら、CharMatcherと関連するtrim~という名前のメソッドを使います。

CharMatcher (Guava: Google Core Libraries for Java HEAD-jre-SNAPSHOT API)

https://google.github.io/guava/releases/snapshot-jre/api/docs/com/google/common/base/CharMatcher.html

String str = "   A B C   "; // ABCの前後に全角/半角空白あり
String trimmed = CharMatcher.whitespace().trimFrom(str);
System.out.println(trimmed); // → "A B C"

この例では文字列の頭と終わりから削除するtrimFromを使っています。他にあるメソッドとしては、trimLeadingFromは最初から削除し、trimTrailingFromは後ろから削除します。

特徴的なのは、対処とする文字の指定の仕方です。CharMatcher.whitespaceだと空白用のCharMatcherを取得し、CharMatcher.asciiならASCIIの範囲にある文字となります。


5.【参考】trim/stripで知っておくと役立つ知識

5-1.コードポイントとはUnicodeでの文字の連番

JavaではUnicodeという方式で文字を扱います。そのUnicodeで、一つの文字ごとに0から順番に割り振られた番号がコードポイントです。UTF-8UTF-16などのやり方(文字符号化方式)でエンコーディングされた結果であるバイト列の値と、コードポイントは違うものです。

Javaのプログラム中で文字を指定する時でも、コードポイントという中立的な数字を使うことがあります。一つの文字のコードポイントはどこでも必ず同じなので、どうエンコーディングされるかには関係なく文字を示せるからです。Javaのプログラム上では、コードポイントは32ビットのintになります。

例えば、半角空白はU+0020というコードポイントが割り当てられています。数字部分は16進数で、これを十進数にすると32です。UTF-8では0x20となり、UTF-16では0x0020です。U+007Fまでは、いわゆるASCIIコードと同じ数字と文字が割り当てられています。

全角空白のコードポイントはU+3000で、UTF-8では0xE3 0x80 0x80となり、UTF-16では0x3000です。いわゆるつちよし(?)のコードポイントはU+20BB7で、UTF-8では0xF0 0xA0 0xAE 0xB7となり、UTF-16BEでは0xD842 0xDFB7です。

5-2.Character.isWhitespaceが空白扱いする文字

空白を判断するCharacter.isWhitespaceは、実はJavaにずっと昔からあるメソッドです。なんと、Java 1.1(11ではない!!)の時代からです。サロゲートペア対応したisWhitespace(int)は、Java 1.5からです

Java 11のJavadocをひも解くと、Character.isWhitespaceは、以下の文字を空白扱いするとあります。

指定された文字(Unicodeコード・ポイント)Javaの基準に従った空白かどうかを判定します。 次の基準のどれかを満たす場合にだけ、Javaの空白文字になります。

Unicodeの空白文字(SPACE_SEPARATORLINE_SEPARATOR、またはPARAGRAPH_SEPARATOR)であるが、改行なしの空白(‘\u00A0’‘\u2007’‘\u202F’)ではない。

‘\t’ (U+0009水平タブ)である。

‘\n’ (U+000A改行)である。

‘\u000B’ (U+000B垂直タブ)である。

‘\f’ (U+000Cフォーム・フィード)である。

‘\r’ (U+000D復帰)である。

‘\u001C’ (U+001Cファイル区切り文字)である。

‘\u001D’ (U+001Dグループ区切り文字)である。

‘\u001E’ (U+001Eレコード区切り文字)である。

‘\u001F’ (U+001F単位区切り文字)である。

さて、コードポイントが書いてあるものはわかりやすいです。でも、SPACE_SEPARATORLINE_SEPARATORPARAGRAPH_SEPARATORなる記述があります。これらは、Unicodeで決められている、同じような種類の文字をまとめた「一般カテゴリ(General Category)」と呼ばれるものです。

それぞれどんな文字が含まれるかは、例えば以下のサイトを参照してください。空白のカテゴリーだけで17個も文字があるのです。世の中は広いですね

Unicode Characters in the ‘Separator, Space’ Category

http://www.fileformat.info/info/unicode/category/Zs/list.htm

→ SPACE_SEPARATORに対応するもの(カテゴリーZs)。全角空白はここに含まれます。ただし、U+00A0U+2007U+202FCharacter.isWhitespaceでは空白扱いされません。

Unicode Characters in the ‘Separator, Line’ Category

https://www.fileformat.info/info/unicode/category/Zl/list.htm

→ LINE_SEPARATORに対応するもの(カテゴリーZl)

Unicode Characters in the ‘Separator, Paragraph’ Category

https://www.fileformat.info/info/unicode/category/Zp/list.htm

→ PARAGRAPH_SEPARATORに対応するもの(カテゴリーZp)


6.まとめ

この記事では、String.trimの使い方から初めて、Java 11で追加されたstripに触れました。そして、自分が欲しいと思うtrimを自分で作ったり、関連する外部ライブラリまでを簡単に紹介しました。

紹介したプログラムはあくまで一例なので、皆さんの必要に応じて参考にしたり、カスタマイズしてください。

Javaの標準APIに無い機能はどんどん自分で作りましょう。標準APIと言えども、皆さんのプログラムと同じように、標準APIにあるクラスを使って作られていることには変わりません。

それに、標準APIがどういう風に実装されているか調べて知ることは、プログラマとしてのスキルアップにもつながる、大事なことですよ。

私たちは、全てのエンジニアに市場価値を高め自身の望む理想のキャリアを歩んでいただきたいと考えています。もし、今あなたが転職を検討しているのであればこちらの記事をご一読ください。理想のキャリアを実現するためのヒントが見つかるはずです。

『技術力』と『人間力』を高め市場価値の高いエンジニアを目指しませんか?

私たちは「技術力」だけでなく「人間力」の向上をもって遙かに高い水準の成果を出し、関わる全ての人々に感動を与え続ける集団でありたいと考えています。

高い水準で仕事を進めていただくためにも、弊社では次のような環境を用意しています。

  • 定年までIT業界で働くためのスキル(技術力、人間力)が身につく支援
  • 「給与が上がらない」を解消する6ヶ月に1度の明確な人事評価制度
  • 平均残業時間17時間!毎週の稼動確認を徹底しているから実現できる働きやすい環境

現在、株式会社ボールドでは「キャリア採用」のエントリーを受付中です。

まずは以下のボタンより弊社の紹介をご覧いただき、あなたの望むキャリアビジョンをエントリーフォームより詳しくお聞かせください。

コメント

公式アカウントLINE限定!ボールドの内定確率が分かる無料診断実施中
公式アカウントLINE限定!
ボールドの内定確率が分かる無料診断実施中