ComparatorインターフェースとJavaの高度なソートシステム
Javaでファイル名をソートすると、file10.txtがfile2.txtの前に表示されることがあります。これは、デフォルトの文字列ソートメカニズムが数値の意味を理解していないためです。この問題を解決するためには、Comparatorインターフェースを使用します。
文字列のソート:デフォルトの動作を超える
文字列は最も一般的なデータ型の一つですが、実際の業務では単純なアルファベット順に並べるだけでは不十分な場合があります。以下にいくつかの例を示します。
辞書順の昇順と降順
文字列をアルファベット順に並べるには、Comparator.naturalOrder()を使用できます。
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class StringSortExample {
public static void main(String[] args) {
List<String> words = Arrays.asList("banana", "apple", "cherry", "date");
words.sort(Comparator.naturalOrder());
System.out.println(words); // [apple, banana, cherry, date]
}
}
降順にする場合は、reversed()メソッドを使用します。
words.sort(Comparator.naturalOrder().reversed());
長さによるソート
文字列の長さでソートするには、Comparator.comparing()を使用します。
List<String> texts = Arrays.asList("hi", "hello", "hey", "greetings", "yo");
texts.sort(Comparator.comparing(String::length));
System.out.println(texts); // [hi, yo, hey, hello, greetings]
同じ長さの場合、次に辞書順でソートするには、thenComparing()を使用します。
texts.sort(Comparator.comparing(String::length).thenComparing(String::compareTo));
大文字小文字を無視したソート
大文字小文字を無視してソートするには、String.CASE_INSENSITIVE_ORDERを使用します。
List<String> mixedCase = Arrays.asList("Zebra", "apple", "Banana", "cherry");
mixedCase.sort(String.CASE_INSENSITIVE_ORDER);
System.out.println(mixedCase); // [apple, Banana, cherry, Zebra]
数値のソート:罠とベストプラクティス
数値のソートも注意が必要です。特に整数や浮動小数点数の場合、適切な比較方法を選択することが重要です。
整数の安全な比較
整数を比較するときには、Integer.compare()を使用します。
List<Integer> nums = Arrays.asList(2_000_000_000, -2_000_000_000);
nums.sort(Integer::compare);
浮動小数点数の特殊値
浮動小数点数を比較するときには、Double.compare()を使用します。
List<Double> doubles = Arrays.asList(Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 1.0, -1.0);
doubles.sort(Double::compare);
混合文字列のスマートソート
文字列中に数値が含まれている場合、自然な順序でソートするために、カスタムのComparatorを作成します。
IntuitiveStringComparatorの実装
public class IntuitiveStringComparator implements Comparator<String> {
@Override
public int compare(String s1, String s2) {
int i1 = 0, i2 = 0;
while (i1 < s1.length() || i2 < s2.length()) {
boolean isDigit1 = i1 < s1.length() && Character.isDigit(s1.charAt(i1));
boolean isDigit2 = i2 < s2.length() && Character.isDigit(s2.charAt(i2));
if (isDigit1 && isDigit2) {
long n1 = extractNumber(s1, i1, new int[1]);
long n2 = extractNumber(s2, i2, new int[1]);
int cmp = Long.compare(n1, n2);
if (cmp != 0) return cmp;
i1 += getNumberLength(s1, i1);
i2 += getNumberLength(s2, i2);
} else if (!isDigit1 && !isDigit2) {
char c1 = i1 < s1.length() ? s1.charAt(i1++) : 0;
char c2 = i2 < s2.length() ? s2.charAt(i2++) : 0;
if (c1 != c2) return Character.compare(c1, c2);
} else {
return isDigit1 ? 1 : -1;
}
}
return Integer.compare(s1.length(), s2.length());
}
private long extractNumber(String s, int start, int[] endHolder) {
int i = start;
while (i < s.length() && Character.isDigit(s.charAt(i))) {
i++;
}
endHolder[0] = i;
String numStr = s.substring(start, i);
return numStr.isEmpty() ? 0 : Long.parseLong(numStr);
}
private int getNumberLength(String s, int start) {
int i = start;
while (i < s.length() && Character.isDigit(s.charAt(i))) {
i++;
}
return i - start;
}
}
実践的なテスト
List<String> files = Arrays.asList(
"data9.csv", "data10.csv", "data1.csv",
"backup_v1.zip", "backup_v10.zip", "backup_v2.zip",
"README.md"
);
files.sort(new IntuitiveStringComparator());
files.forEach(System.out::println);
出力:
README.md
backup_v1.zip
backup_v2.zip
backup_v10.zip
data1.csv
data9.csv
data10.csv
順序付きコレクションでの持続的な使用
カスタムComparatorは、一時的なソートだけでなく、TreeSetやTreeMapのコンストラクタ引数としても使用できます。
Set<String> sortedFiles = new TreeSet<>(new IntuitiveStringComparator());
sortedFiles.addAll(Arrays.asList("file2", "file10", "file1", "file3"));
System.out.println(sortedFiles); // [file1, file2, file3, file10]
同様に、TreeMapでも使用できます。
Map config = new TreeMap<>(new IntuitiveStringComparator());
config.put("threshold10", "high");
config.put("threshold2", "low");
config.put("threshold1", "critical");
config.forEach((k, v) -> System.out.println(k + " -> " + v));
これにより、集合やマップが常に期待通りの順序を保つことができます。