前回は継承について説明しました。今回はインターフェースと例外処理について説明します。
Java のインターフェースはメソッドの仕様 (抽象メソッド) だけを定義した抽象クラスの一種ですが、「多重継承」できるところが他のクラスと異なるところです。
クラスを多重継承するとき複雑な問題を引き起こす場合がありますが、その問題点について把握しておくと、インターフェースの理解が深まると思います。そこで、まず最初に多重継承の問題点について説明します。
一般的なオブジェクト指向の場合、継承によって引き継がれる性質は定義されたデータ (インスタンス変数など) やメソッドになります。これを「実装の継承」と呼びます。また、インスタンス変数を継承することを特別に「属性の継承」と呼ぶ場合があります。
多重継承を行う場合、異なる性質や機能を持つクラスを継承することがあります。たとえば、クラス Foo にはメソッド methodA() があり、クラス Bar にはメソッド methodB() があるとしましょう。この 2 つのメソッドはまったく異なる働きをするとします。ここで、メソッド methodA() はインスタンス変数 x を使っていて、methodB() も x を使っていると、多重継承で問題が発生します。
一般的な多重継承で、クラス Foo と Bar を継承してクラス Baz を作成した場合、クラス Baz のインスタンスに変数 x は一つしか存在しません。メソッド methodA() と methodB() は一つしかない x を使うことになります。この場合、どちらかのメソッドは正常に動作しないでしょう。これでは多重継承する意味がありませんね。
また、多重継承ではインスタンス変数だけではなく、メソッド名が衝突する場合もあります。このように、多重継承では名前の衝突が発生する危険性があるのです。それから、多重継承にはもう一つ問題点があります。それはクラスの階層構造が複雑になることです。
単一継承の場合、クラスの階層は木構造になりますが、多重継承ではグラフになります。木構造の場合、クラスの優先順位は簡単に決めることができますが、グラフになると優先順位を決めるためのアルゴリズムは複雑になり、それを理解するのは難しくなります。多重継承は強力な機能ですが、使うときには十分な注意が必要になるのです。
ちなみにC++の場合、多重継承したクラスに同名のメソッドがある場合、どちらを呼び出すのか明確に指定しないとコンパイルでエラーとなります。またC++はメンバ変数も継承されるため、変数名の衝突も発生します。この場合も、どちらの変数を使用するのか明確に指定しないとコンパイルエラーとなります。
このほかにも、多重継承ではいろいろな問題が発生するため、それを解決するためにC++ではいろいろな機能が用意されています。ところが、それらの機能がC++をいっそう複雑な言語にしていると M.Hiroi には思えてなりません。C++はコンパイラ型の言語で、なによりも効率を重視するため、複雑な言語仕様になるのは避けられないのかもしれません。
これらの問題を回避するため、属性を継承するスーパークラスは一つだけに限定して、あとのスーパークラスは実装だけを継承するという方法があります。これを Mix-in といいます。
具体的には、インスタンス変数を定義せずにメソッドだけを記述したクラスを用意します。属性の継承は単一継承になりますが、実装のみを記述したクラスはいくつ継承してかまいません。一つのクラスに複数の実装を混ぜることから Mix-in と呼ばれています。
なお、Mix-in は特別な機能ではなく、多重継承を使いこなすための方法論にすぎません。多重継承を扱うことができるプログラミング言語であれば Mix-in を行うことが可能です。この Mix-in という方法を言語仕様に取り込んでいるのが Ruby です。Mix-in を図 1 に示します。
A / B Mixin A / \ Mixin B \ / \ / C D 図 1 : Mix-in
クラス C はクラス B を継承していて、そこにクラス Mixin A が Mix-in されています。クラス D もクラス B を継承していますが、Mix-in されているクラスは Mixin B となります。
多重継承の問題点は Mix-in ですべて解決できるわけではありませんが、クラスの階層構造がすっきりとしてわかりやすくなることは間違いありません。Mix-in は多重継承を使いこなす優れた方法だと思います。
Java は単一継承なので Mix-in を使うことはできませんが、そのかわりに「インターフェース (interface)」という機能が用意されています。Java のインターフェースはメソッドの「仕様」だけを記述した抽象クラスのことで、クラスは複数のインターフェースを継承することができます。このように、メソッドの仕様だけを継承する方法を「仕様の継承」といいます。
属性を継承しないところは Mix-in と同じですが、メソッドの実装さえも継承しないところがインターフェースの特徴です。したがって、メソッドの実体はインターフェースを継承したクラスでプログラムしないといけません。また、インターフェースは抽象クラスの一種と考えられるので、データ型として使用することができます。つまり、クラスをインターフェースの型に変換することで、ポリモーフィズムを有効に活用することができるわけです。
それでは、具体的にインターフェースの使い方を説明しましょう。インターフェースの定義は interface 文で行います。interface の構文を示します。
interface name extends superInterface1, ... { TypeA methodA(TypeB args, ...); ... }
interface の後ろに名前を指定します。インターフェースは他のインターフェースを継承することができます。インターフェースは多重継承できるので、extends の後ろに複数のスーバーインターフェースを指定することができます。そして、ブロックの中でメソッドを宣言します。メソッドの実体を定義することはできません。インターフェースで宣言されたメソッドは暗黙のうちに abstract と public が付加されます。
クラスでインターフェースを継承するには implements 文を使います。implements 文の構文を示します。
class name extends superClass implements interface1, ... { ... }
extends でスーパークラスを指定し、implements でインターフェースを指定します。スーパークラスは一つしか指定できませんが、インターフェースは複数指定することができます。
それでは簡単な例として、前回作成した図形のプログラムをインターフェースを使って書き直してみましょう。次のリストを見てください。
リスト : インターフェースの使い方 (sample70.java) // 図形のインターフェース interface IFigure { String kindOf(); double area(); void print(); } // 三角形 class Triangle implements IFigure { double altitude, base; Triangle(double a, double b) { altitude = a; base = b; } public String kindOf() { return "Triangle"; } public double area() { return altitude * base / 2.0; } public void print() { System.out.print("Triangle : area = " + area()); } } // 四角形 class Rectangle implements IFigure { double width, height; Rectangle(double w, double h) { width = w; height = h; } public String kindOf() { return "Rectangle"; } public double area() { return width * height; } public void print() { System.out.print("Rectangle : area = " + area()); } } // 円 class Circle implements IFigure { double radius; Circle(double r) { radius = r; } public String kindOf() { return "Circle"; } public double area() { return radius * radius * Math.PI; } public void print() { System.out.print("Circle : area = " + area()); } } public class sample70 { static double sumOfArea(IFigure[] ary) { double sum = 0.0; for (IFigure a: ary) sum += a.area(); return sum; } public static void main(String[] args) { IFigure[] a = { new Triangle(2.0, 2.0), new Rectangle(2.0, 2.0), new Circle(2.0), }; System.out.println(sumOfArea(a)); } }
$ javac sample70.java $ java sample70 18.566370614359172
最初に図形を操作するインターフェース IFigure を定義します。インターフェースの名前はクラスと同じ付け方でかまいませんが、クラスと区別したい場合は先頭に I を付けることがあります。
この中でメソッド kindOf(), area(), print() を宣言します。次に図形を表すクラス Triangle, Rectangle, Circle を定義します。ここで implements で IFigure を継承します。そして、各クラスで IFigure のメソッドを実装します。@Override は付けていませんが、IFigure のメソッドがすべて実装されていないと、そのクラスは抽象クラスとみなされるため、コンパイルでエラーになります。
クラスは継承したインターフェースのデータ型にアップキャストすることができます。main() では IFigure 型の配列 a を宣言し、Triangle, Rectangle, Circle のインスタンスで初期化します。どのクラスも IFigure を継承しているので配列 a に格納することができます。
面積の合計を求める関数 sumOfArea() は引数に IFigure 型の配列を受け取ります。そして、IFigure に定義されているメソッドを使ってインスタンスを操作することができます。sumOfArea() はメソッド area() を呼び出していますが、ポリモーフィズムの働きにより各クラスのメソッド area() が呼び出されるので図形の面積を正しく求めることができます。
Java には便利なインターフェースがあらかじめ用意されています。簡単な例題として、Comparable インターフェースを紹介しましょう。Comparable インターフェースを implements するとデータの順序関係を使ったメソッドを利用することができます。たとえば、パッケージ java.util.Arrays のスタティックメソッド sort() を使ってデータをソートすることができます。
Comparable には次のメソッドが宣言されています。
int compareTo(Object obj);
compareTo() は自分自身のオブジェクト (this) と引数の obj を比較し、obj が大きい場合は負の整数、等しい場合は 0、小さい場合は正の整数を返します。
それでは図形の抽象クラス IFigure に Comparable インターフェースを実装してみましょう。次のリストを見てください。
リスト : Comparable の使用例 (sample71.java) import java.util.Arrays; // 抽象クラス abstract class Figure implements Comparable { abstract String kindOf(); abstract double area(); void print() { System.out.print(kindOf() + ": area = " + area()); System.out.println(); } @Override public int compareTo(Object o) { double r = area() - ((Figure)o).area(); if(r < 0.0) return -1; else if (r == 0.0) return 0; return 1; } } // 三角形 class Triangle extends Figure { double altitude, base; Triangle(double a, double b) { altitude = a; base = b; } @Override String kindOf() { return "Triangle"; } @Override double area() { return altitude * base / 2.0; } } // 四角形 class Rectangle extends Figure { double width, height; Rectangle(double w, double h) { width = w; height = h; } @Override String kindOf() { return "Rectangle"; } @Override double area() { return width * height; } } // 円 class Circle extends Figure { double radius; Circle(double r) { radius = r; } @Override String kindOf() { return "Circle"; } @Override double area() { return radius * radius * Math.PI; } } public class sample71 { static double sumOfArea(Figure[] ary) { double sum = 0.0; for (Figure a: ary) sum += a.area(); return sum; } public static void main(String[] args) { Figure[] a = { new Triangle(5.0, 5.0), new Circle(2.0), new Rectangle(2.0, 2.0), }; Arrays.sort(a); for (Figure f: a) f.print(); System.out.println(sumOfArea(a)); } }
$ javac sample71.java $ java sample71 Rectangle: area = 4.0 Triangle: area = 12.5 Circle: area = 12.566370614359172 29.066370614359172
抽象クラス Figure でインターフェース Comparable を継承します。このクラスでメソッド compareTo() を実装します。自分自身の面積は area() で求めることができます。引数 o は Object 型なので、o の前に (Figure) を付けてダウンキャストします。Object はすべてのクラスのスーパークラスなので、どのクラスにでもダウンキャストさせることができます。これで Figure 型に変換されるので、メソッド area() を呼び出すことができます。あとは、大きさを比較して -1, 0, 1 を返すだけです。
メソッド sort() はパッケージ java.util のクラス Arrays に定義されているので、import java.util.Arrays; で Arrays をインポートします。これで Arrays.sort(a); とすればでメソッド sort() を呼び出すことができます。インポートしない場合、java.util.Arrays.sort(a) で呼び出すことができます。Figure はインターフェース Comparable を継承しているので、配列 a は Arrays.sort(a) でソートすることができます。
Java はどんなクラスでも明示的にキャストすればインターフェースへ型変換することができます。ただし、コンパイルに成功したとしても、キャストしたオブジェクトが実際にそのインターフェースを実装していなければ、プログラムを実行した時にエラーが発生します。
簡単な例を示しましょう。次のリストを見てください。
リスト : インターフェースへのキャスト (sample72.java) interface Foo { void bar(); } class Baz1 implements Foo { public void bar() { System.out.println("oops!"); } } class Baz2 { } public class sample72 { public static void main(String[] args) { Baz1 a = new Baz1(); Baz2 b = new Baz2(); Foo c = a; Foo d = (Foo)b; c.bar(); d.bar(); } }
インターフェース Foo を定義します。クラス Baz1 は Foo を継承し、メソッド bar() を定義します。クラス Baz2 は Foo を継承せず、メソッド bar() も定義していません。次に、main() で Baz1 と Baz2 のインスタンスを生成します。Baz1 は Foo を継承しているので、キャストしなくても Foo に型変換することができます。Baz2 は Foo を継承していませんが、(Foo) と明示的にキャストすることで、Foo の変数 d に代入することができます。
このプログラムは正常にコンパイルできますが、実行すると次のようにエラーが発生します。
$ javac sample72.java $ java sample72 Exception in thread "main" java.lang.ClassCastException: class Baz2 cannot be cast to class Foo (Baz2 and Foo are in unnamed module of loader 'app') at sample72.main(sample72.java:16)
Baz2 はインターフェース Foo を実装していないので Foo にキャストすることはできない、というわけです。このように、キャストを間違えるとエラーが発生するので注意してください。
簡単な例として、図形のクラスで Figure をインターフェース (IFigure) として定義し、そこに Comparable を継承してみましょう。プログラムは次のようになります。
リスト : Comparable の使用例 (sample73.java) import java.util.Arrays; // インターフェース interface IFigure extends Comparable { String kindOf(); double area(); void print(); } // 三角形 class Triangle implements IFigure { double altitude, base; Triangle(double a, double b) { altitude = a; base = b; } public String kindOf() { return "Triangle"; } public double area() { return altitude * base / 2.0; } public void print() { System.out.print("Triangle : area = " + area()); System.out.println(); } public int compareTo(Object o) { double r = area() - ((IFigure)o).area(); if(r < 0.0) return -1; else if (r == 0.0) return 0; return 1; } } // 四角形 class Rectangle implements IFigure { double width, height; Rectangle(double w, double h) { width = w; height = h; } public String kindOf() { return "Rectangle"; } public double area() { return width * height; } public void print() { System.out.print("Rectangle : area = " + area()); System.out.println(); } public int compareTo(Object o) { double r = area() - ((IFigure)o).area(); if(r < 0.0) return -1; else if (r == 0.0) return 0; return 1; } } // 円 class Circle implements IFigure { double radius; Circle(double r) { radius = r; } public String kindOf() { return "Circle"; } public double area() { return radius * radius * Math.PI; } public void print() { System.out.print("Circle : area = " + area()); System.out.println(); } public int compareTo(Object o) { double r = area() - ((IFigure)o).area(); if(r < 0.0) return -1; else if (r == 0.0) return 0; return 1; } } public class sample73 { static double sumOfArea(IFigure[] ary) { double sum = 0.0; for(IFigure a: ary) sum += a.area(); return sum; } public static void main(String[] args) { IFigure[] a = { new Triangle(5.0, 5.0), new Circle(2.0), new Rectangle(2.0, 2.0), }; Arrays.sort(a); for(IFigure x: a) x.print(); System.out.println(sumOfArea(a)); } }
$ javac sample73.java $ java sample73 Rectangle : area = 4.0 Triangle : area = 12.5 Circle : area = 12.566370614359172 29.066370614359172
IFigure はインターフェースなので、extends で Comparable を継承します。あとは各クラスでメソッド compareTo() を実装するだけです。このとき、compareTo() の引数 Object o をインターフェース IFigure にキャストします。これでメソッド area() を呼び出すことができます。
メソッド main() では IFigure 型の配列 a を用意し、各クラスのインスタンスで初期化します。各クラス は Comparable を実装しているので、IFigure 型の配列はメソッド Arrays.sort() でソートすることができます。
一般に、例外 (exception) はエラー処理で使われる機能です。「例外=エラー処理」と考えてもらってもかまいません。最近は例外処理を備えているプログラミング言語が多くなりました。もちろん Java にも例外処理があります。なお、エラーが発生したことを「例外が発生した」とか「例外が送出された」という場合もあります。本稿でもエラーのことを例外と記述することにします。
通常、例外が発生すると Java はプログラムの実行を中断しますが、致命的な例外でなければプログラムの実行を継続する、または特別な処理を行わせたい場合もあるでしょう。このような場合にこそ、例外処理が役に立つのです。Java では発生した例外を捕まえるのに try 文を使います。try 文の構文を図 2 に示します。
try { 処理A; } catch(ExceptionClass 引数) { 処理B; } 図 2 : 例外処理
try 文は、そのあとに定義されている処理 A を実行します。処理 A が正常に終了した場合は try 文も終了します。もしも、処理 A で例外が発生した場合、処理 A の実行は中断され、その例外が catch 節で指定した例外と一致すれば、その catch 節を実行します。
catch 節には例外をクラス ExceptionClass で指定します。また、try 文には複数の catch 節を指定することができます。Java の場合、例外は Throwable というクラスとして定義されています。例外は階層構造になっていて、すべての例外は直接または間接的に Throwable を継承します。Throwable は Error と Exception に分けられ、Exception は RuntimeException とそれ以外の例外に分けられます。
Error を継承した例外は、復旧するのが困難なエラーが発生したことを表します。RuntimeException を継承した例外は、Java の仮想マシン (JVM) で発生したエラーを表します。たとえば、0 で割ったときに送出される例外 ArithmeticException や、配列の添字が範囲外であることを表す例外 ArrayIndexOutOfBoundsException などがあります。
Error と RuntimeException は「非チェック例外」といって、try 文で例外処理を記述しなくてもプログラムをコンパイルすることができます。ところが、RuntimeException 以外の Exception は「チェック例外」といって、try 文で例外処理を記述しないとコンパイルでエラーになります。たとえば、入出力処理でファイルにアクセスするとき、何らかの理由で失敗すると例外が送出されますが、これらの例外は Exception のサブクラスとして定義されています。
try 文の使い方は簡単です。次のリストを見てください。
リスト : 例外処理の使用例 (sample74.java) public class sample74 { static int foo(int a, int b) { try { return a / b; } catch(ArithmeticException e) { System.out.println(e); return 0; } } public static void main(String[] args) { System.out.println(foo(10, 2)); System.out.println(foo(10, 0)); } }
関数 foo(a, b) は a / b を返します。Java の場合、0 で除算すると例外 ArithmeticException を送出して実行を中断します。ここで、try 文の catch 節に ArithmeticException を指定すると、例外を捕捉して処理を続行することができます。
実行例を示します。
$ javac sample74.java $ java sample74 5 java.lang.ArithmeticException: / by zero 0
foo(10, 2) は 5 を返しますが、foo(10, 0) は 0 で除算しているので例外 ArithmeticException が送出されます。この例外クラスは catch 節に指定されているので、その節が実行されて例外クラスのインスタンス e の内容を表示して 0 を返します。
例外は throw で送出することができます。
throw new ExceptionClass(args, ...);
throw には例外クラスのインスタンスを引数として渡します。throw が実行されると、プログラムの実行を直ちに中断して、例外を受け止める catch 節があると、そこへ制御が移ります。該当する catch 節がない場合、プログラムの実行は中断されます。
簡単な例を示しましょう。
リスト : 例外の送出 (sample75.java) public class sample75 { public static void main(String[] args) { try { throw new RuntimeException("Oops!"); } catch(RuntimeException e) { System.out.println(e); } } }
$ javac sample75.java $ java sample75 java.lang.RuntimeException: Oops!
例外に渡した引数は、例外クラスのインスタンスに格納されます。例外クラスのインスタンスは try 文の catch 節で受け取ることができます。上記の例では、送出された例外のインスタンスは変数 e にセットされます。例外に渡したメッセージはインスタンスに格納されます。
例外は他の例外を継承することで、ユーザが独自に定義することができます。非チェック例外でよければ、RuntimeException を継承するといいでしょう。次の例を見てください。
リスト : 例外の定義 (sample76.java) class FooException extends RuntimeException { FooException() { } FooException(String msg) { super(msg); } } public class sample76 { public static void main(String[] args) { try { throw new FooException("Oops!"); } catch(FooException e) { System.out.println(e); } } }
$ javac sample76.java $ java sample76 FooException: Oops!
このように、FooException は RuntimeException を継承しているので、フィールド変数やメソッドを定義しなくても適当なコンストラクタを用意するだけで動作します。
Java の例外は、try 文の中で呼び出した関数の中で例外が送出されても、それを捕捉することができます。この機能を使って、評価中の関数からほかの関数へ制御を移す「大域脱出 (global exit)」を実現することができます。
簡単な例を示しましょう。
リスト : 大域脱出 (sample77.java) class ExitException extends RuntimeException { ExitException() {} ExitException(String msg) { super(msg); } } public class sample77 { static void bar1() { System.out.println("call bar1"); } static void bar2() { throw new ExitException("Global Exit"); } static void bar3() { System.out.println("call bar3"); } static void foo(){ bar1(); bar2(); bar3(); } public static void main(String[] args) { try { foo(); } catch(ExitException e) { System.out.println(e); } } }
try 文で関数 foo() を実行すると、次のようになります。
$ javac sample77.java $ java sample77 call bar1 ExitException: Global Exit
実行の様子を図 3 に示します。
┌───────┐ │try { ... } │←─┐ │catch { ... } │ │ └───────┘ │ ↓ │ ┌──────┐ │ │ foo() │──┐│ └──────┘ ││ ↓↑ ↓│ ┌──────┐ ┌ bar2() ──────┐ │ bar1() │ │throw ExitException │ └──────┘ └──────────┘ 図 3 : 大域脱出
通常の関数呼び出しは、呼び出し元の関数に制御が戻ります。ところが bar2() で throw が実行されると、呼び出し元の関数 foo() を飛び越えて、制御が try 文の catch 節に移るのです。このように、例外処理を使って関数を飛び越えて制御を移すことができます。
大域脱出はとても強力な機能ですが、多用すると処理の流れがわからなくなる、いわゆる「スパゲッティプログラム」になってしまいます。使用には十分ご注意下さい。
Exception を継承すると、その例外は「チェック例外」になります。チェック例外は必ず try 文で例外処理を行う必要があります。ところが、その場で処理をするよりも、呼び出し元のメソッドに戻ってから処理をしたほうが都合がよい場合もあります。このような場合、throws で送出される例外を宣言することにより、その処理を他のメソッドに任せることができます。
throws は次のように宣言します。
データ型 メソッド名(データ型 引数, ...) throws 例外クラス名, ... { 処理; ... }
たとえば、大域脱出用の例外クラス ExitException のスーパークラスを Exception に変更すると、ExitException はチェック例外になります。プログラムをそのままコンパイルすると、次のようにコンパイルエラーになります。
$ javac sample77.java sample77.java:12: エラー: 例外ExitExceptionは報告されません。スローするには、捕捉または宣言する必要があります throw new ExitException("Global Exit"); ^ sample77.java:28: エラー: 例外ExitExceptionは対応するtry文の本体ではスローされません } catch(ExitException e) { ^ エラー2個
そこで、throws で ExitExcptions を送出することを宣言します。プログラムは次のようになります。
リスト : throws の使用例 (sample78.java) class ExitException extends Exception { ExitException() {} ExitException(String msg) { super(msg); } } public class sample78 { static void bar1() { System.out.println("call bar1"); } static void bar2() throws ExitException { throw new ExitException("Global Exit"); } static void bar3() { System.out.println("call bar3"); } static void foo() throws ExitException { bar1(); bar2(); bar3(); } public static void main(String[] args) { try { foo(); } catch(ExitException e) { System.out.println(e); } } }
$ javac sample78.java $ java sample78 call bar1 ExitException: Global Exit
メソッド foo() と bar2() で throws ExitException を追加します。これで正常にコンパイルすることができます。
ところで、プログラムの途中で例外が送出されると、残りのプログラムは実行されません。このため、必要な処理が行われない場合があります。このような場合、try 文に finally 節を定義します。finally 節は try 文の処理で例外が発生したかどうかにかかわらず、try 文の処理が終了するときに必ず実行されます。例外が発生した場合は、finally 節を実行したあとで同じ例外を再送出します。
なお、catch 節と finally 節を同時に try 文に書く場合は、catch 節を先に定義してください。そのあとで finally 節を定義します。
簡単な例を示しましょう。大域脱出で作成した foo() を呼び出す関数 baz() を作ります。
リスト : finally 節 (sample79.java) class ExitException extends RuntimeException { ExitException() {} ExitException(String msg) { super(msg); } } public class sample79 { static void bar1() { System.out.println("call bar1"); } static void bar2() { throw new ExitException("Global Exit"); } static void bar3() { System.out.println("call bar3"); } static void foo() { bar1(); bar2(); bar3(); } static void baz() { try { foo(); } finally { System.out.println("celan up"); } } public static void main(String[] args) { try { baz(); } catch(ExitException e) { System.out.println(e); } } }
実行すると次のようになります。
$ javac sample79.java $ java sample79 call bar1 celan up ExitException: Global Exit
関数 bar2() で送出された例外 ExitError は baz() の ensure 節で捕捉されて print 'clean up' が実行されます。その後、例外 ExitError が再送出され、main() の catch 節に捕捉されて Global Exit と表示されます。
分数の四則計算を行うクラス Rational を定義してください。なお、分子と分母には BigInteger を使ってください。
リスト : 解答例 (q07.java) import java.math.BigInteger; // 分数 (有理数) クラス class Rational implements Comparable { final BigInteger numerator; // 分子 final BigInteger denominator; // 分母 // コンストラクタ Rational(BigInteger x, BigInteger y) { if (y.compareTo(BigInteger.ZERO) < 0) { x = x.negate(); // 分子に符号をつける y = y.negate(); } var z = x.gcd(y); numerator = x.divide(z); denominator = y.divide(z); } Rational(long x, long y) { this(BigInteger.valueOf(x), BigInteger.valueOf(y)); } // 四則演算 // a/b + c/d = (a * d + c * b) / (b * d) Rational add(Rational n) { var x = numerator.multiply(n.denominator); var y = denominator.multiply(n.numerator); var z = denominator.multiply(n.denominator); return new Rational(x.add(y), z); } // a/b - c/d = (a * d - c * b) / (b * d) Rational subtract(Rational n) { var x = numerator.multiply(n.denominator); var y = denominator.multiply(n.numerator); var z = denominator.multiply(n.denominator); return new Rational(x.subtract(y), z); } // a/b * c/d = (a * c) / (b * d) Rational multiply(Rational n) { var x = numerator.multiply(n.numerator); var y = denominator.multiply(n.denominator); return new Rational(x, y); } // a/b / c/d = (a * d) / (b * c) Rational divide(Rational n) { var x = numerator.multiply(n.denominator); var y = denominator.multiply(n.numerator); return new Rational(x, y); } @Override public int compareTo(Object o) { var n = (Rational)o; var x = numerator.multiply(n.denominator); var y = denominator.multiply(n.numerator); return x.compareTo(y); } // 文字列に変換 @Override public String toString() { if (denominator.compareTo(BigInteger.ONE) == 0) { return numerator.toString(); } else { return numerator + "/" + denominator; } } } public class q07 { public static void main(String[] args) { var a = new Rational(1, 2147483648L); var b = new Rational(1, 4294967296L); System.out.println(a); System.out.println(b); System.out.println(a.add(b)); System.out.println(a.subtract(b)); System.out.println(b.subtract(a)); System.out.println(a.multiply(b)); System.out.println(a.divide(b)); System.out.println(b.divide(a)); System.out.println(a.compareTo(b)); System.out.println(a.compareTo(a)); System.out.println(b.compareTo(a)); } }
$ javac q07.java $ java q07 1/2147483648 1/4294967296 3/4294967296 1/4294967296 -1/4294967296 1/9223372036854775808 2 1/2 1 0 -1