問題3-11
小学生を表現するSchoolChildクラスは、不完全です。
また、小学生クラスを使用するCompareBasicPracticeクラスのmainメソッドも不完全です。
CompareBasicPracticeクラスは以下の部分でコンパイルエラーになります。
Collections.sort(classroom);
Collectionsユーティリティクラスのsortメソッドは、引数で渡されたList型の要素をソートします。
しかし、不完全なためソートできません。
次の指示に従って、2種類のソートを可能にしてください。
【SchoolChildクラス】
「順序の基準」を設定するために、SchoolChildクラスにComparable
このインタフェースは、compareToという抽象メソッドがあるので、適切に実装してください。
ただし、整列のルールは「出席番号の小さい順」にします。
【CompareBasicPracticeクラス】
Collectionsユーティリティクラスのsortメソッドには、第二引数に「特別な順序の基準」を設定するためのComparator
このインタフェースをそのままインスタンス化することはできませんが、無名(匿名)クラスを利用することで可能となります。
このインタフェースが持つcompare抽象メソッドを、無名クラスで実装してください。
ただし、整列のルールは「身長の高い順」にします。
【実行結果】
整列前 出席番号:4 名前:武藤 身長:145.0 出席番号:2 名前:佐藤 身長:130.0 出席番号:5 名前:加藤 身長:162.5 出席番号:1 名前:伊藤 身長:155.3 出席番号:3 名前:江藤 身長:151.1 出席番号順 出席番号:1 名前:伊藤 身長:155.3 出席番号:2 名前:佐藤 身長:130.0 出席番号:3 名前:江藤 身長:151.1 出席番号:4 名前:武藤 身長:145.0 出席番号:5 名前:加藤 身長:162.5 身長の高い順 出席番号:5 名前:加藤 身長:162.5 出席番号:1 名前:伊藤 身長:155.3 出席番号:3 名前:江藤 身長:151.1 出席番号:4 名前:武藤 身長:145.0 出席番号:2 名前:佐藤 身長:130.0
【CompareBasicPractice.java】
import java.util.*; // CompareBasicPracticeは未完成です public class CompareBasicPractice { public static void main(String[] args) { // 小学生たちがいる教室を表現するArrayList型変数 ArrayList<SchoolChild> classroom = new ArrayList<SchoolChild>(); // 5人の生徒が教室にいます classroom.add(new SchoolChild(4, "武藤", 145.0)); classroom.add(new SchoolChild(2, "佐藤", 130.0)); classroom.add(new SchoolChild(5, "加藤", 162.5)); classroom.add(new SchoolChild(1, "伊藤", 155.3)); classroom.add(new SchoolChild(3, "江藤", 151.1)); // 生徒たちの情報を表示(整列前) System.out.println("整列前"); for(SchoolChild sc : classroom) { System.out.println(sc); } System.out.println(); // 先生からの指示「出席番号順に整列しなさい」 Collections.sort(classroom); // 生徒たちの情報を表示(出席番号順) System.out.println("出席番号順"); for(SchoolChild sc : classroom) { System.out.println(sc); } System.out.println(); // 先生からの指示「身長の高い順に整列しなさい」 Collections.sort(classroom); // 生徒たちの情報を表示(身長の高い順) System.out.println("身長の高い順"); for(SchoolChild sc : classroom) { System.out.println(sc); } } } // 小学生クラスは未完成です class SchoolChild { private int no; // 出席番号 private String name; // 名前 private double height; // 身長 // コンストラクタ public SchoolChild(final int no, final String name, final double height) { this.no = no; this.name = name; this.height = height; } // 出席番号の取得 public int getNo() { return no; } // 名前の取得 public String getName() { return name; } // 身長の取得 public double getHeight() { return height; } @Override public String toString() { return "出席番号:" + no + " 名前:" + name + " 身長:" + height; } }
解答例
【CompareBasicPractice.java】
import java.util.*; public class CompareBasicPractice { public static void main(String[] args) { // 小学生たちがいる教室を表現するArrayList型変数 ArrayList<SchoolChild> classroom = new ArrayList<SchoolChild>(); // 5人の生徒が教室にいます classroom.add(new SchoolChild(4, "武藤", 145.0)); classroom.add(new SchoolChild(2, "佐藤", 130.0)); classroom.add(new SchoolChild(5, "加藤", 162.5)); classroom.add(new SchoolChild(1, "伊藤", 155.3)); classroom.add(new SchoolChild(3, "江藤", 151.1)); // 生徒たちの情報を表示(整列前) System.out.println("整列前"); for(SchoolChild sc : classroom) { System.out.println(sc); } System.out.println(); // 先生からの指示「出席番号順に整列しなさい」 Collections.sort(classroom); // 生徒たちの情報を表示(出席番号順) System.out.println("出席番号順"); for(SchoolChild sc : classroom) { System.out.println(sc); } System.out.println(); // 先生からの指示「身長の高い順に整列しなさい」 Collections.sort(classroom, new Comparator<SchoolChild>() { public int compare(SchoolChild sc1, SchoolChild sc2) { // 身長の高い者が先に来るようにする return sc1.getHeight() == sc2.getHeight() ? 0 : (sc1.getHeight() > sc2.getHeight() ? -1 : 1); } }); // 生徒たちの情報を表示(身長の高い順) System.out.println("身長の高い順"); for(SchoolChild sc : classroom) { System.out.println(sc); } } } // 小学生クラス class SchoolChild implements Comparable<SchoolChild> { private int no; // 出席番号 private String name; // 名前 private double height; // 身長 // コンストラクタ public SchoolChild(final int no, final String name, final double height) { this.no = no; this.name = name; this.height = height; } // 出席番号の取得 public int getNo() { return no; } // 名前の取得 public String getName() { return name; } // 身長の取得 public double getHeight() { return height; } @Override public String toString() { return "出席番号:" + no + " 名前:" + name + " 身長:" + height; } public int compareTo(SchoolChild sc) { // 出席番号が小さい者が先に来るようにする return no == sc.getNo() ? 0 : (no > sc.getNo() ? 1 : -1); } }
解説
業務システムでは、データのソート(整列)は非常に重要です。
Javaにも、もちろんソートに関する基本的な仕組みがあります。
それが今回の問題のテーマです。
最初の段階のソースでは、以下のコードでコンパイルエラーが発生します。
// 先生からの指示「出席番号順に整列しなさい」 Collections.sort(classroom);
つまり、小学生たちに「自然順序付けに従って整列してください」と要求しているわけです。
しかし、小学生たちは理解できません(コンパイルエラーのことです)。
何故かというと、この小学生たちはまだ何を基準に整列すれば良いか先生に教わっていないからです。
さて、今回の小学生クラスは「出席番号」「名前」「身長」の3つのフィールドを持っています。
その中で「自然順序付け」は当然「出席番号」ですよね。
そのためには、Comparableインタフェースを利用します。
「Compare」は「比較」、「able」は「可能」と言うことなので、このインタフェースを実装したクラスのインスタンスは「自分たちだけで比較可能」になります。
今回の問題では、まずはSchoolChildクラスにComparable
このインタフェースには次のような抽象メソッドが1つあります。
int compareTo(T o);
引数のT型は、ジェネリックスによりSchoolChild型に置換されます。
「compareTo」という英語は、日本語に解釈すると「〜と比較する」という意味です。
つまり、ある小学生自分自身と、引数で渡ってきた対戦相手の小学生と勝負して、勝ち負けを決めるわけです。
戻り値のint型は勝敗の結果を返します。
もし自分が負ければ(順序的に小さいと判断されれば)負の値を返し、自分が勝てば(順序的に大きいと判断されれば)正の値を返します。
引き分けの場合は(順序的に同じと判断されれば)0を返します。
この戻り値の絶対値的な大小には特に意味がなく、通常は「-1」「0」「1」のいずれかを返すのが基本です。
今回は「出席番号順」ですから、出席番号の小さい方が負けになるわけです。
次のようなコードになります。
public int compareTo(SchoolChild sc) { // 出席番号が小さい者が先に来るようにする return no == sc.getNo() ? 0 : (no > sc.getNo() ? 1 : -1); }
条件演算子をネストすることで、「-1」「0」「1」のいずれかを返しています。
単純に return no - sc.getNo();
でも動作しますが、とてつもなく大きな値や小さな値を扱う場合、オーバーフローして符号が逆転してしまう可能性があるので注意が必要です。
さて、今回はもう一つ課題がありました。
小学校で整列と言えば、基本は確かに「出席番号順」ですが、場合によっては別の整列を行いたい場合もあります。
良くあるパターンが体育の時間の「身長順」でしょう。
では、小学生クラスにまたComparableインタフェースを実装するのでしょうか?
そんなことはできません。
Comparableインタフェースによる整列はいわゆる「自然順序付け」と呼ばれるもので、もっともオーソドックスなものを一つだけ設定可能です。
今回のような特別な整列には、整列を行わせるための審判のような人を作成します。
その審判の作成には、Comparatorインタフェースを利用します。
「Compare」に「tor」を付けると、まさしく「比較を行う人」という意味になります。
その審判オブジェクトを作成するには、普通はComparatorインタフェースを実装したクラスを作成しますが、今回のようなちょっとだけ機能を使いたい場合は無名クラスを使うと便利です。
Comparatorインタフェースには次のような抽象メソッドが1つあります。
int compare(T o1, T o2);
引数のT型は、Comparableインタフェースの時と同様にSchoolChild型に置換されます。
今回、本人は審判なので自分は比較対象ではありません。
ですので、SchoolChild型の引数が2つあるのです。
第一引数は、言うなれば赤コーナーのチャンピオン小学生、第二引数は青コーナーの挑戦者小学生といった感じです。
第一引数の小学生チャンピオンと表現したのは、戻り値のint型は第一引数の小学生の勝敗結果を返すからです。
もしチャンピオンが負ければ(順序的に小さいと判断されれば)負の値を返し、チャンピオンが勝てば(順序的に大きいと判断されれば)正の値を返します。
引き分けの場合は(順序的に同じと判断されれば)0を返します。
無名クラスは、名前のない「その場しのぎ」なクラスです。
インタフェースや抽象クラスはnew演算子でインスタンス化できませんが、コンストラクタの直後に{}を記述することで、名前のないサブクラスを定義できるのです。
// 先生からの指示「身長の高い順に整列しなさい」 Collections.sort(classroom, new Comparator<SchoolChild>() { public int compare(SchoolChild sc1, SchoolChild sc2) { // 身長の高い者が先に来るようにする return sc1.getHeight() == sc2.getHeight() ? 0 : (sc1.getHeight() > sc2.getHeight() ? -1 : 1); } });
無名クラスは、最初はすごく違和感を感じます。
しかし、Android等のGUIアプリ開発ではよく使用されますので早くなれるようにしましょう。
あと、注意点としては、何らかの数値を大きい順に整列する場合、数値の大きいものから表示する、つまり数値の大きい方を順序的には小さい扱いにする必要があると言うことです。
慣れないと、頭の中が混乱しそうになりますので十分注意してください。
ここで少し補足ですが、Java8では無名クラスのインスタンス生成と抽象メソッドの実装を簡単に行える「ラムダ式」という画期的な概念が導入されました。
今回は「ラムダ式」を扱いませんが、これからは非常に重要になってきます。
ちなみに、上記コードをラムダ式で書くと次のようになります。
// 先生からの指示「身長の高い順に整列しなさい」 Collections.sort(classroom, (sc1, sc2) -> sc1.getHeight() == sc2.getHeight() ? 0 : (sc1.getHeight() > sc2.getHeight() ? -1 : 1));
JavaBeans等の値クラスにComparableインタフェースを実装すると、非常に扱いやすいものになります。
例えば、配列に関するユーティリティクラスArraysにも、自然順序付けでソートを行うためのsortメソッドがありますし、格納要素を自動的に自然順序付けでソートする機能を持つTreeSetクラスやTreeMapクラスなどもあります。
参考図書
LINE公式アカウント
仕事が辛くてたまらない人生が、仕事が楽しくてたまらない人生に変わります。
【登録いただいた人全員に、無料キャリア相談プレゼント中!】