Javaについて徹底解説!

compareToでJavaのソートは自由自在! 一からお伝えします

JavaのインターフェイスComparableのメソッドcompareToでは、大小比較を行います。このインターフェイスを実装したクラスは大小比較ができるようになり、結果、ソートを行えるようになるのです。

どんなプログラムでも、ソートは欠かせない処理です。JavaのソートはこのComparableと、もう一つの大小比較のインターフェイスであるComparatorが支えています。

この記事では、Comparable.compareToについて初心者向けに一から説明します。便利な使い方やComparatorとの使い分けについても説明しますので、ぜひ読んでいってください。

なお「compareToは後から勉強するので、今はとにかくソートのやり方を!!」という方は、「4.Comparableを使ったソート」がまさにそれです!!

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

1.Comparable.compareToはソートで使う

1-1.Comparable.compareToは大小比較をする

インターフェイスComparableの抽象メソッドcompareToは、自分自身と引数のクラスを大小比較した結果を整数で戻すメソッドです。

まずはcompareToの使い方と戻り値について、以下だけしっかりと覚えておけば、compareToを使う上ではバッチリです!!

使い方

   比較するもの.compareTo(比較されるもの)

   →大小比較した結果の整数が戻る

大小比較の結果

   比較するもの < 比較されるもの:-1以下の整数

   比較するもの = 比較されるもの:0

   比較するもの > 比較されるもの:1以上の整数

1-2.ソートには大小比較が必要不可欠!

配列やコレクションのソートは頻繁に使います。何かを日付別にソートしたり、集計結果の件数などで昇順/降順にソートしたりなどです。

そんなソートを行うには、配列やコレクションに格納されている要素同士の大小比較が必要です。バブルソート、マージソート、クイックソートなど、どんなソートアルゴリズムを使うにしても、大小比較は欠かせません。

intなどの数字なら大小比較は自明です。でもJavaはオブジェクト指向プログラミング言語なので、クラス同士の大小比較が必要ですが、クラスには数字のように自明な判断基準がないので、プログラマーが大小を教えてあげなければなりません。そのために使うのがComparable.compareTo()です。

2.Comparable.compareToのコツ

2-1.判断結果は小さい・等しい・大きいの3種類だけ

まずは、compareToがどういうものか見ていきましょう。Comparable.compareToのメソッド宣言は以下のようになっています。

ここで、実用上で意識すべきなのは、戻り値が0かどうか、そして0より大きいか小さいかです。

説明には負の整数・正の整数とありますが、単に0と比べて大きいのか小さいのかを知りたいだけすなわち3つの状態を区別したいだけなので、数字の絶対値自体には意味はありません。一般的には-101が使われますし、JDKのソースコードでも(大体は)同じです。

2-2.大小比較は相手より自分が大きいか小さいか

覚えておくべきなのは、比較「する」側と比較「される」側の関係です。比較する側のcompareToは、比較される側を引数に呼び出されます。大小比較の結果はこれを前提とした戻り値となります。

つまり、比較する側「から」見た視点になります。自分が比較される側より大きいか小さいか、あるいは同じかが、compareToの戻り値となります。

例えば、数値の50が比較する側、100が比較される側だとします。50からすると自分は100よりも「小さい」ので、戻り値は-1以下の整数になります。

2-3.IntegerでcompareToを試してみる

さて、前述のとおり、Comparableはインターフェイスなので、何かのクラスに実装(implements)しなければなりません。intのラッパークラスIntegerComparableを実装していますので、これでどういう結果になるか試してみます。

きちんとメソッドの仕様どおりの戻り値になっていますね。これがcompareToの動きです。Comparableを実装している同じクラス同士なら、全てこのように大小比較ができるのです。

※「6.【参考】Java標準APIのクラスでのcompareToについて」に、Integer以外のJava標準APIの良く使うクラスでcompareToを行った場合にどうなるかを記載してあります。

3.Comparable.compareToの実装例

3-1.compareToの基本形

前章ではcompareToをIntegerなどで試してみました。次は自分で作ったクラスでcompareToしてみましょう。以下は何かのユーザ(User)のつもりで、int noはユーザごとの番号です。この番号をキーにcompareToしてみます。

これがcompareToの基本形です。compareToの引数は比較先の何かのインスタンスです。でも、型がObjectのままでは比較しようがないので、自分自身のクラスにキャストしてから比較に使う値を参照し、大小比較の結果を戻します。

この例では整数の常識に従った大小比較をしています。でも、これは皆さんの好きにできるJavaのプログラムですので、いかようにも大小比較のルールを自分で設定できるのです!! 大小比較の結果、正負の整数と0のどれかを戻すというルールを守りさえすればいいのです。

例えば、このクラスの大小比較結果を番号の「降順」としたいなら、大小比較で戻す整数の正負を逆転させるだけです。

なお、Comparableの型引数(<>の中にあるもの)をきちんと使うなら以下のようになるでしょう(以後はこの書き方で行きます)Java 1.5以降を使われているならこちらがお勧めです。

しかし、型引数を使ったとしても、行っていることは本質的には何も変わっていません。型引数を使うことでキャストの手間が省けるのと、型引数で指定したクラス以外をcompareToの引数にするとコンパイルエラーになるので、より安全性が高まったというくらいです。

3-2.複数の条件で大小比較をしたい場合

前節では一つの項目のみで大小比較をしてみました。実際のプログラミングでは複数の項目を組み合わせて大小比較をすることが多いので、そのようなものも実装してみましょう。

例として、先ほどのクラスUserに氏名(name)を追加して、氏名の昇順番号の降順で大小比較をするcompareToを実装してみます。

最初にすべきは名前の大小比較です。StringはComparableですので、compareToを実行できます。名前が違えば(!= 0)大小は既に確定しましたので、比較結果でreturnします。

名前の大小比較が「等しい」なら、さらに番号で大小比較します。intである番号のcompareToの結果を正負反転させてreturnすれば、番号の降順の比較結果を戻すことになります。ここで、正負を反転させるために-1を掛けるのは、compareToでの常套手段です。

このように、複数の項目を組み合わせる場合は、compareToの中で一つ一つの項目の大小比較を行って結果を確認しながら、全体としての大小比較を行っていけばよいのです。

3-3.プリミティブ型の大小比較はcompareで楽しよう!

先ほどInteger.compare(int, int)というメソッドを使いました。これは、int同士の大小比較をcompareToの方法で実行した結果を返してくれるメソッドです。ですから、実は自分でわざわざif文を書かなくてもいいのです。楽ちんですね。

Java 7以降では、プリミティブ型のラッパークラスにはこのような便利な大小比較メソッドがあるので、どんどん活用しましょう。

3-4.比較できないなら例外をthrowしよう

Comparable.compareToのJavadocをよく読むと、以下の記述があります。

書いてあるのはもっともなことで、引数の比較先のインスタンスがnullなら比較のしようがありませんし、自分と関係のないクラスと比較することにも意味がありませんので、そんな場合は例外をthrowして「比較ができないよ」と表明します。

先のUserは自動的にそうなりますが、きちんと明示的にプログラムするなら以下のようになります。

4.Comparableを使ったソート

さて、Comparable.compareToがどんなものか、どうプログラムすればいいのかをご理解いただいたところで、実際にComparableのソートをしてみましょう。

ここではComparableのサンプルとして、分かりやすさからStringを使います。でも、IntegerDateBigDecimalや、この記事で作成したUserのようなComparableを実装した任意のクラスでも同じやり方が使えます。要は、ソート対象がComparableでさえあればいいのです!

4-1.Arrays.sort(Object[])で配列をソートする

Comparableなものの配列をソートするなら、Arrays.sort(Object[])を使います。引数はObjectの配列ですが、事実上Comparableが実装されていて、かつ全要素が同じクラスのインスタンスでなければ、実行時に例外がthrowされます。

Arrays.sort(Object[])は「小さい」順にソートします。「大きい」順にソートしたい場合は少し工夫が必要で、Collections.reverseOrder()またはComparator.reverseOrder()を併用します。もちろん、小さい・大きいはComparableでプログラムしたとおりの順序になります。

このメソッドでは引数で与えた配列自体の並び順が変わります。ソートされた配列を戻り値で受け取るスタイルではないので、気を付けましょう。

開始・終了インデックスの引数があるメソッドは、ソートする範囲を指定できるものです。配列全体ではなく、部分的にソートしたい場合に使えますね。

4-2.Collections.sort(List)/reverse(List)でListをソートする

Comparableを格納しているListをソートする際は、Collections.sort(List)/reverse(List)が使えます。逆順にするメソッドがあらかじめ用意されているのが便利ですね。Arraysと同様に引数で与えたList自体の並び順が変わります。

4-3.Stream.sorted()でStreamの結果をソートする

Streamの中間操作sorted()を使ってもソートができます。逆順にする場合は、Arraysでも出てきたCollections.reverseOrder()あるいはComparator.reverseOrder()を使います。他の中間操作を行った結果をソートすることも朝飯前なので、色々と柔軟にソートを行えます。

4-4.TreeSet/TreeMapからソート済の値/キーを取得する

TreeSetとTreeMapは少し特殊なSetMapで、値・キーを取り出す時は、Comparableでの並び順にしてくれます。TreeSetは値の自動ソート機能が付いたSetTreeSetはキーの自動ソート機能が付いたMapとして覚えておいてもいいでしょう。

ここで一つ注意事項を。TreeSetTreeMapを使う時は、TreeSetに投入する値・TreeSetで使うキーについて制限があります。比較する2つのインスタンス(e1e2)に対して「e1.equals(e2)trueの場合は、e1.compareTo(e2)0を戻す」を満たさなければなりません。

ですので、Javaの標準APIにあるクラスを使う分にはほとんど問題ありませんが、自作のComparableを使う場合は、制限を満たしているか確認しましょう。標準クラスでの例外は、例えばBigDecimalです。

このように、Object.equalsComparable.compareToの間には、実はちょっとした関係があったりします。これは「自然順序付けがequalsと一貫性がある」とか「compareToで課せられる順序はequalsと一致している」などと言われます。ここでは詳細は延べませんが、興味があれば調べてみると、理解が深まるでしょう。

5.【関連】Comparator.compareの使い方

Comparable.compareToと関係のあるものとして、インターフェイスComparatorのメソッドcompareがあります。

戻り値のintの意味はcompareToと同じで、正負の整数と0による大小比較結果です。ただし、compareToが自分自身と比較先インスタンスを比較していたのに対し、2つの引数同士を比較するのが異なるところです。

Comparatorは、ソートしたいクラスからソートのロジック部分を分離するのが主な用途でしょう。ソートを行いたいクラスにComparatorを明示的に実装するのは見かけませんし、意味がありません。使い方も無名クラスやラムダ式で用いられるケースも多いように感じます。

例えば、以下のようにして使います。Comparatorにソートのロジック部分を分離することで、自由にソートのロジックを切り替えられるのです。デフォルトのソートロジックはComparable.compareToで実装して、その他の特殊ケースはComparatorと役割分担することもできます。

6.【参考】Java標準APIのクラスでのcompareToについて

Javaの標準APIにある、良く使われるクラスではどういう基準で大小比較されるのかを簡単にまとめます。これらのクラスは全てComparableを実装しているので、大小比較ができる(=ソートできる)のです。

  • Byte/Short/Integer/Long:保持する整数の大小
  • Float/Double:保持する小数点付き数値の大小
  • BigDecimal/BigInteger:保持する数値の大小
  • Boolean:falsetrueよりも小さいと判断される
  • Character:保持する文字のUnicodeコードポイントの数値の大小
  • String:保持する文字列全体に対して、先頭の文字からCharacterのロジックを用いて大小比較する(辞書順)
  • Date/Calendar/LocalDate/LocalDateTime等:保持する日付・日時がカレンダー的に過去なら小さい、未来なら大きい
  • Enum:定数として宣言された順序値(ordinal()で得られる整数値)の大小で判断

数値のクラスは数字の大小、日付・日時は早い順での判断になります。Booleanは特徴的なので、ぜひ覚えておきましょう。EnumcompareToは使う箇所は少ないかもしれませんね。

文字や文字列は、いわゆる「辞書順」での比較が行われます。直感的にはアルファベット順などの並び順だと思ってもらっても、実用上は構いません。実際には、それぞれの文字に割り当てられているUnicodeコードポイントという数値での比較です。文字・文字列は本質的には数値の集まりだからです。

6-1.Dateはミリ秒までの時間を持っている

DateやCalendarはミリ秒で時間を保持しているので、compareToではミリ秒までが大小比較に反映されます。Dateを日付として扱うなら、compareToする前に時間以下を0でクリアしないと正しく大小比較されません。

以下のような時間以下を0にするメソッドを作っておいてもいいでしょう。ただ、Java 7からのLocalDateTime/ZonedDateTimeなどにはこのような機能が標準でありますし(truncateTo)、日付だけを持つクラスもあるので(LocalDate)、新しいプログラムならそちらを使うのがお勧めです。

6-2.BigDecimalのcompareToは精度を意識しない

BigDecimalは、保持する数字の表現が複数あります。特に小数点以下の精度が違うケースがあります。でもcompareToの場合は単純に数値としての大小・等しいかどうかが判断されます。equals代わりにcompareToを使おうとすると、結果が違うことがあるので要注意です。

6-3.符号なし整数の大小比較には専用のメソッドがある

Java 8から、整数の符号で使っている1ビットも数値の範囲に入れて計算するための機能追加が行われました(Unsigned Arithmetic Support)。その流れで、大小比較でも正負の符号を意識せずに大小を比較したい場合が出て来ています。

そのような目的のために、Byte/Short/Integer/Longの整数型ラッパークラスへは、compareUnsignedが追加されました。

7.まとめ

この記事では、Comparable.compareToについてお伝えしてきました。compareTo2つのインスタンスの間の大小比較を行い、結果を小さい・等しい・大きいのいずれかで戻すメソッドです。

書いてしまうと単純なものですが、compareToJavaのソート処理の根幹を支えるとても重要なメソッドです。さらに、オブジェクト間の同一性にも関係するメソッドであったりします。縁の下の力持ち的な感じですね。

compareToの基本をしっかりと身に着けて、Javaでのソート処理を自由自在に書けるようになりましょう。

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

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

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

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

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

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