はじめに (対象読者・この記事でわかること)

この記事は、Javaでオブジェクト指向プログラミングを学びたい初学者〜中級者の方を対象にしています。特に、数学的なオブジェクト(今回は球体)をクラスとして表現したい方におすすめです。

この記事を読むことで、Javaで球体を表現するためのBallクラスの設計・実装方法がわかります。加えて、球体の体積・表面積を計算するメソッドの実装や、オブジェクト指向の基本概念(カプセル化・情報隠蔽)を実践的に学べます。3Dグラフィックスやシミュレーション入門の第一歩としても活用できます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - Javaの基本的な文法(クラス・メソッド・変数) - オブジェクト指向プログラミングの基本概念(クラスとインスタンス) - 数学:円周率πと半径を使った球体の体積・表面積の公式

Javaで球体を表現する意義

ゲーム開発、3Dモデリング、物理シミュレーションなどでは、球体は最も基本的かつ重要な幾何学的オブジェクトです。Javaで球体を扱う際、単に半径を保持するだけでなく、体積計算、衝突判定、可視化など多岐にわたる処理が必要になります。

これらを毎回個別に実装していては、コードの重複・保守性の低下・バグの温床となります。そこで、球体を一つの「オブジェクト」としてまとめたBallクラスを作成することで、再利用可能で拡張性の高いプログラム構造を実現できます。

また、Ballクラスを通じて、カプセル化(データと処理を一つにまとめる)、情報隠蔽(外部からの直接アクセスを制限する)、コネクション(不変条件を保つ)など、オブジェクト指向の重要な概念を実践的に学べます。

Ballクラスの設計と実装

ここでは、段階的にBallクラスを構築していきます。まずは最小限の機能から始め、徐々に拡張していきましょう。

ステップ1:基本構造とフィールドの定義

まず、球体を表現するために必要な最小限の情報は「半径」と「中心座標」です。2次元・3次元に応じて座標の数は変わりますが、ここでは3次元を前提に進めます。

Java
public class Ball { private double radius; // 半径 private double x, y, z; // 中心座標 // 定数 private static final double MIN_RADIUS = 0.0; private static final double DEFAULT_RADIUS = 1.0; // コンストラクタ public Ball(double radius, double x, double y, double z) { setRadius(radius); // バリデーション付き this.x = x; this.y = y; this.z = z; } // デフォルトコンストラクタ public Ball() { this(DEFAULT_RADIUS, 0.0, 0.0, 0.0); } }

ポイントは、フィールドをすべてprivateにして外部からの直接アクセスを禁止することです。これにより、Ballクラス内部の実装を自由に変更できるようになります(情報隠蔽)。

ステップ2:ゲッター・セッターとバリデーション

次に、各フィールドに安全にアクセスするためのメソッドを実装します。特に、半径に負の値を設定できないようバリデーションを加えます。

Java
public class Ball { // 省略:フィールドとコンストラクタ // === 半径関連 === public double getRadius() { return radius; } public void setRadius(double radius) { if (radius < MIN_RADIUS) { throw new IllegalArgumentException( "半径は" + MIN_RADIUS + "以上である必要があります。入力値:" + radius ); } this.radius = radius; } // === 座標関連 === public double getX() { return x; } public double getY() { return y; } public double getZ() { return z; } public void setPosition(double x, double y, double z) { this.x = x; this.y = y; this.z = z; } public void move(double dx, double dy, double dz) { this.x += dx; this.y += dy; this.z += dz; } }

setRadiusで例外をスローすることで、Ballオブジェクトが常に有効な状態(半径0以上)を保っています。これを「不変条件を守る」といいます。

ステップ3:体積・表面積の計算メソッド

球体の体積と表面積を返すメソッドを追加しましょう。数学的な公式をプログラムに落とし込む典型的な例です。

Java
public class Ball { // 省略:フィールド・コンストラクタ・ゲッター・セッター /** * 球体の体積を返す * 公式:V = 4/3 * π * r^3 */ public double getVolume() { return (4.0 / 3.0) * Math.PI * Math.pow(radius, 3); } /** * 球体の表面積を返す * 公式:S = 4 * π * r^2 */ public double getSurfaceArea() { return 4.0 * Math.PI * radius * radius; } }

Math.PIで円周率、Math.powで累乗計算を行っています。メソッド名にgetを付けることで、「この値を取得するだけで副作用はない」という意図を明確にしています。

ステップ4:他のBallオブジェクトとの関係メソッド

2つの球体の「中心間距離」や「重なり(衝突)判定」はよく使う処理です。Ballクラス内部に実装しておくと便利です。

Java
public class Ball { // 省略:フィールド・コンストラクタ・ゲッター・セッター・体積表面積 /** * 他のBallとの中心間距離を返す */ public double distanceTo(Ball other) { double dx = this.x - other.x; double dy = this.y - other.y; double dz = this.z - other.z; return Math.sqrt(dx*dx + dy*dy + dz*dz); } /** * 他のBallと衝突しているか(重なっているか)判定 * 中心間距離が両半径の和より小さい=衝突 */ public boolean isCollidingWith(Ball other) { double distance = this.distanceTo(other); return distance < (this.radius + other.radius); } /** * 他のBallを完全に内包しているか */ public boolean contains(Ball other) { double distance = this.distanceTo(other); // 中心間距離 + 相手半径 < 自分半径 なら内包 return (distance + other.radius) < this.radius; } }

このようにBallクラス内部に「球体同士の関係性」を記述することで、利用側は個別に距離計算や不等式を書く必要がなくなり、コードが読みやすくなります。

ステップ5:toString・equals・hashCodeのオーバーライド

Javaでは、オブジェクトの文字列表現・等価性・ハッシュ値を適切にオーバーライドしておくことで、デバッグやコレクション利用が楽になります。

Java
public class Ball { // 省略:上記までの実装 @Override public String toString() { return String.format( "Ball(r=%.2f, center=(%.2f,%.2f,%.2f))", radius, x, y, z ); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Ball)) return false; Ball other = (Ball) obj; return Double.compare(radius, other.radius) == 0 && Double.compare(x, other.x) == 0 && Double.compare(y, other.y) == 0 && Double.compare(z, other.z) == 0; } @Override public int hashCode() { return Objects.hash(radius, x, y, z); } }

toStringを実装しておけば、System.out.println(ball);のように即座に内容を確認できます。equalshashCodeを揃えて実装しておけば、List#containsHashMapのキーとしても正しく動作します。

ハマった点やエラー解決

  1. 浮動小数点の誤差で衝突判定が不安定になる - 現象:中心間距離と半径の和を==で比較すると、わずかな誤差で判定が反転 - 原因:浮動小数は正確に表現できない - 解決:Math.abs(distance - (r1+r2)) < 1e-9のように誤差を許容する

  2. 半径0の球体を許可すべきか - 検討:数学的には半径0の球は「点」であり、体積・表面積も0で整合性が取れる - 結論:ドメインによります。物理シミュレーションなら0は許可(質点として扱う)、レンダリング用途なら0は非表示とするため許可してよい

  3. 性能面での懸念 - 球体が何千もあるシミュレーションでは、毎フレームdistanceTo→sqrtがボトルネックになる - 改善:衝突判定のみなら比較の対象を二乗にしてsqrtを回避できる java public boolean isCollidingWith(Ball other) { double dx = this.x - other.x; double dy = this.y - other.y; double dz = this.z - other.z; double rSum = this.radius + other.radius; return (dx*dx + dy*dy + dz*dz) < (rSum*rSum); }

解決策

上記のように、浮動小数の比較には誤差許容を設け、パフォーマンスが重要な箇所では平方根を避ける工夫をするとよいです。また、不変条件(半径>=0)を守るため、セッターで例外をスローする実装を徹底しましょう。

まとめ

本記事では、Javaで球体を表現するBallクラスを設計・実装しました。フィールドのカプセル化、バリデーション付きセッター、体積・表面積・衝突判定メソッド、そして有用なオーバーライドメソッドを段階的に導入することで、安全で再利用しやすいクラスが完成しました。

  • カプセル化:データと処理を一つにまとめ、外部からの直接アクセスを禁止
  • 不変条件:半径0以上というルールを守ることで、Ballオブジェクトが常に有効
  • 拡張性:距離計算・衝突判定を内包しているため、ゲーム・シミュレーションに直結

このBallクラスをベースに、質量・速度・色・テクスチャなどを追加すれば、より実用的な3Dオブジェクトへと発展させられます。次回は、Ballオブジェクトを複数扱う「ボウリングシミュレーション」を作ってみたいと思います。

参考資料