Java/文法/クラス
クラス
[編集]概要
[編集]クラスは、オブジェクト指向プログラミング(OOP)における基本的な構成要素です。データ(フィールド)と振る舞い(メソッド)を組み合わせた抽象データ型を定義する枠組みを提供します。
クラスの主要な特徴
[編集]カプセル化(Encapsulation)
[編集]- データとメソッドを1つの単位にまとめる
- 内部実装の隠蔽
- データの整合性とセキュリティの向上
- アクセス制御の実現
継承(Inheritance)
[編集]- 既存クラスの機能を再利用
- コードの再利用性向上
- クラス階層の構築
- 「is-a」関係の表現
ポリモーフィズム(Polymorphism)
[編集]- 同じインターフェースで異なる実装を提供
- メソッドのオーバーライド
- 型の柔軟な取り扱い
- 拡張性の向上
抽象化(Abstraction)
[編集]- 実世界の概念のモデル化
- 実装詳細の隠蔽
- インターフェースの提供
- 複雑さの管理
Javaにおけるクラスの構造
[編集]クラスの基本構文
[編集]public class MyClass { // フィールド(状態) private int value; // コンストラクタ public MyClass(int initialValue) { this.value = initialValue; } // メソッド(振る舞い) public void setValue(int newValue) { this.value = newValue; } }
クラスのメンバー
[編集]フィールド
[編集]- インスタンス変数
- オブジェクトごとに固有の状態を保持
- インスタンス生成時にメモリ割り当て
- クラス変数(static)
- クラス全体で共有される状態
- クラスロード時にメモリ割り当て
メソッド
[編集]- インスタンスメソッド
- オブジェクトの振る舞いを定義
- thisキーワードでインスタンスを参照
- クラスメソッド(static)
- クラス全体で共有される機能
- インスタンス不要で呼び出し可能
コンストラクタ
[編集]- オブジェクト生成時の初期化処理
- クラス名と同名
- オーバーロード可能
- 継承時のsuper()呼び出し
アクセス制御
[編集]アクセス修飾子
[編集]修飾子 | クラス内 | 同一パッケージ | サブクラス | 全てのクラス |
---|---|---|---|---|
public | ○ | ○ | ○ | ○ |
protected | ○ | ○ | ○ | × |
(default) | ○ | ○ | × | × |
private | ○ | × | × | × |
final修飾子
[編集]- 変数:値の変更を禁止
- メソッド:オーバーライドを禁止
- クラス:継承を禁止
高度な機能
[編集]ネストしたクラス
[編集]- 静的ネストクラス
- 内部クラス
- ローカルクラス
- 匿名クラス
初期化ブロック
[編集]- インスタンス初期化ブロック
- 静的初期化ブロック
Generics対応
[編集]public class Container<T> { private T value; public void set(T value) { this.value = value; } public T get() { return value; } }
ベストプラクティス
[編集]設計原則
[編集]- 単一責任の原則
- カプセル化の徹底
- 適切な粒度の設定
- インターフェースの活用
コーディング規約
[編集]- 命名規則の遵守
- アクセス制御の適切な使用
- コメントとドキュメンテーション
- コードの整形と可読性
型推論
[編集]型推論は、コンパイラが変数の型を自動的に推測する機能です。
Java 10からは、var
キーワードを使用してローカル変数の型を推論することができるようになりました。
以下は、型推論を使用したインスタンス化の例です。
var obj = new MyClass(10);
この場合、コンパイラはobj
の型をMyClass
と推論します。
明示的に型を宣言する必要がなくなり、コードの冗長性を減らすことができます。
継承
[編集]Javaでは、既存のクラスを拡張して新しいクラスを作成することができます。 これを継承と呼びます。 継承を使用することで、既存のクラスのフィールドやメソッドを再利用し、新しいクラスで追加や修正を加えることができます。
public class ChildClass extends ParentClass { // ChildClass のフィールドとメソッドを定義 }
多重継承
[編集]Javaでは、クラスは単一の直接スーパークラスしか持つことができません(単一継承)。 しかし、インタフェースは複数実装できるため、多重継承の代替手段として使用されます。
シールドクラス (Sealed class)
[編集]シールドクラス (Sealed class) は、Java 17から導入された新機能の1つです。 シールドクラスは、そのサブクラスがどのクラスになるかを制限するために使用されます。 シールドクラスは以下のように定義されます:
sealed class Shape permits Circle, Rectangle { // クラスの定義 }
上記の例では、Shape
というシールドクラスを定義し、Circle
とRectangle
というサブクラスを許可しています。つまり、Circle
とRectangle
以外のクラスはShape
のサブクラスになることができません。
シールドクラスを使用することで、クラスの階層構造を制御し、より安全な設計を実現することができます。 これにより、クラスの派生元や使用方法をより明確に定義することができます。
ポリモーフィズム
[編集]概要
[編集]ポリモーフィズムは、同一のインターフェースで異なる実装を提供する機能です。「多態性」とも呼ばれ、オブジェクト指向プログラミングの重要な柱の一つです。
種類
[編集]コンパイル時ポリモーフィズム
[編集]- 定義: コンパイル時に解決される静的な多態性
- 実現方法: メソッドオーバーロード
- 特徴:
- 同じメソッド名で異なるパラメータ
- コンパイル時に決定
- 静的バインディング
メソッド定義 | 説明 |
---|---|
void print(int x) |
整数を出力 |
void print(String s) |
文字列を出力 |
void print(double d) |
小数を出力 |
実行時ポリモーフィズム
[編集]- 定義: 実行時に解決される動的な多態性
- 実現方法: メソッドオーバーライド
- 特徴:
- 継承関係に基づく
- 実行時に決定
- 動的バインディング
class Animal { void makeSound() { System.out.println("Some sound"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Woof"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Meow"); } }
メソッドオーバーライド
[編集]基本ルール
[編集]- メソッド名が同一
- 引数リストが同一
- 戻り値の型が同一または共変
- アクセス修飾子は同一または緩和
オーバーライドの例
[編集]public class Parent { protected void display() { System.out.println("Parent's display"); } } public class Child extends Parent { @Override public void display() { // アクセス修飾子を緩和 System.out.println("Child's display"); } }
@Override アノテーション
[編集]- オーバーライドを明示
- コンパイル時チェック
- コード可読性の向上
抽象化の仕組み
[編集]抽象クラス
[編集]- 特徴:
- インスタンス化不可
- 抽象メソッドと具象メソッドの混在
- 単一継承のみ
public abstract class Shape { protected String color; // 具象メソッド public void setColor(String color) { this.color = color; } // 抽象メソッド public abstract double calculateArea(); }
インターフェース
[編集]- 特徴:
- すべてのメソッドが抽象(Java 8以降はdefaultメソッド可)
- 定数のみ保持可能
- 多重実装可能
public interface Drawable { void draw(); // 抽象メソッド default void print() { // デフォルトメソッド System.out.println("Printing..."); } }
ジェネリクス
[編集]基本概念
[編集]- 型パラメータによる汎用プログラミング
- コンパイル時の型安全性確保
- コード再利用性の向上
ジェネリッククラスの例
[編集]public class Container<T> { private T value; public Container(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } }
型パラメータの制約
[編集]制約 | 説明 | 例 |
---|---|---|
上限境界 | extends による制約 | <T extends Number>
|
下限境界 | super による制約 | <T super Integer>
|
複数境界 | & による連結 | <T extends Number & Comparable>
|
使用例
[編集]Container<String> stringContainer = new Container<>("Hello"); Container<Integer> intContainer = new Container<>(42); String str = stringContainer.getValue(); // 型安全 Integer num = intContainer.getValue(); // 型安全
アクセサ
[編集]アクセサ (Accessor) は、オブジェクトのフィールドにアクセスするためのメソッドのことです。フィールドへの直接アクセスを避け、ゲッターとセッターを用いることで、データのカプセル化と整合性を保ちます。
ゲッター (Getter)
[編集]ゲッターはフィールドの値を取得するためのメソッドで、通常「get
+ フィールド名」の形式を取ります。
public class MyClass { private int number; public int getNumber() { return number; } }
この例では、getNumber
メソッドが number
フィールドの値を返します。
セッター (Setter)
[編集]セッターはフィールドの値を設定するためのメソッドで、通常「set
+ フィールド名」の形式を取ります。
public class MyClass { private int number; public void setNumber(int number) { this.number = number; } }
セッターはフィールドの値を外部から安全に変更できるようにします。
ベストプラクティス
[編集]クラスの設計におけるベストプラクティスを以下に示します。
- 単一責任の原則を守る: クラスは一つの責任または役割だけを持つべきです。
- カプセル化を徹底する: フィールドを
private
で宣言し、アクセサを使用して外部アクセスを制御します。 - 継承を適切に利用する: 継承は親子関係が明確な場合にのみ使用し、インターフェースやコンポジションも検討します。
- 不変性を推奨する: フィールドを
final
で宣言するなどして、クラスの不変性を維持します。 - 適切な命名を心掛ける: クラスやメソッドに意味のある一貫した名前を付けます。
- コンストラクタを適切に設計する: 必要な初期化を効率的に行い、引数の数を最小限にします。
- コメントとドキュメントを追加する: コードの意図や使い方を明確に記述します。
これらを守ることで、保守性が高く拡張性のあるクラスを構築できます。
SOLIDは以下の5つの原則からなります。
- 単一責任の原則(Single Responsibility Principle, SRP):
- 1つのクラスは1つの責務(役割)を持つべきであり、変更する理由は1つであるべきです。この原則により、クラスはシンプルで理解しやすくなり、変更に強い設計になります。
- オープン/クローズドの原則(Open/Closed Principle, OCP):
- ソフトウェアのエンティティ(クラス、モジュール、関数など)は、拡張に対して開いており、修正に対して閉じているべきです。すなわち、新しい機能を追加するときは既存のコードを変更せずに拡張できるようにするべきです。
- リスコフの置換原則(Liskov Substitution Principle, LSP):
- サブタイプ(派生クラス、実装クラス)は、その親タイプ(基底クラス、インターフェース)と置換可能でなければなりません。つまり、基底クラス型の変数に対して派生クラスのインスタンスを代入しても動作が変わらないことを保証する必要があります。
- インターフェース分離の原則(Interface Segregation Principle, ISP):
- クライアント(利用者)が利用しないメソッドに依存しないようにするために、クライアントにとって不要なインターフェースを実装しないように分割します。クライアントは必要な機能にのみ依存するべきです。
- 依存性逆転の原則(Dependency Inversion Principle, DIP):
- 上位のモジュールは下位のモジュールに依存すべきではなく、両方が抽象に依存すべきです。また、具象クラスにではなく抽象クラスやインターフェースに依存すべきです。これにより、コードの柔軟性が向上し、依存関係の注入(DI)などのパターンが可能になります。
コード例
[編集]Javaクラスと配列の操作例
[編集]- 配列
/** * Javaクラスと配列の操作例 */ import java.util.*; /** * 人物を表すクラス */ class Person { // フィールド private String name; private int age; /** * コンストラクタ * @param name 名前 * @param age 年齢 */ public Person(String name, int age) { this.name = name; this.age = age; } /** * 名前のゲッター * @return 名前 */ public String getName() { return name; } /** * 名前のセッター * @param name 名前 */ public void setName(String name) { this.name = name; } /** * 年齢のゲッター * @return 年齢 */ public int getAge() { return age; } /** * 年齢のセッター * @param age 年齢 */ public void setAge(int age) { this.age = age; } } /** * メインクラス */ public class Main { /** * メインメソッド * @param args コマンドライン引数 */ public static void main(String[] args) { // ArrayListを使用してPersonオブジェクトのリストを作成 List<Person> peopleList = new ArrayList<>(); // Personオブジェクトをリストに追加 peopleList.add(new Person("Alice", 30)); peopleList.add(new Person("Bob", 25)); peopleList.add(new Person("Charlie", 35)); // 配列の要素を出力 for (Person person : peopleList) { System.out.println("Name: " + person.getName() + ", Age: " + person.getAge()); } } }
行列クラスの実装例
[編集]- 行列
import java.util.*; /** * 行列演算を行うMatrixクラスの定義 */ import java.util.Iterator; import java.util.NoSuchElementException; import java.util.function.BinaryOperator; /** * 行列を表すMatrixクラス */ class Matrix { /** 行数 */ private final int rows; /** 列数 */ private final int cols; /** 行列データ */ private final double[] data; /** * 行列のコンストラクタ * @param rows 行数 * @param cols 列数 */ public Matrix(int rows, int cols) { this.rows = rows; this.cols = cols; this.data = new double[rows * cols]; } /** * 行列のコンストラクタ * @param data 行列データ */ public Matrix(double[][] data) { this.rows = data.length; this.cols = data[0].length; this.data = new double[rows * cols]; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { this.data[cols * i + j] = data[i][j]; } } } /** * 行数を取得するメソッド * @return 行数 */ public int getRows() { return rows; } /** * 列数を取得するメソッド * @return 列数 */ public int getCols() { return cols; } /** * 指定された位置の要素を取得するメソッド * @param i 行インデックス * @param j 列インデックス * @return 指定された位置の要素 */ public double get(int i, int j) { return data[cols * i + j]; } /** * 指定された位置に要素を設定するメソッド * @param i 行インデックス * @param j 列インデックス * @param value 設定する値 */ public void set(int i, int j, double value) { data[cols * i + j] = value; } /** * 他の行列との加算を行うメソッド * @param other 加算する行列 * @return 加算結果の行列 */ public Matrix add(Matrix other) { return operate(other, Double::sum); } /** * 他の行列との減算を行うメソッド * @param other 減算する行列 * @return 減算結果の行列 */ public Matrix subtract(Matrix other) { return operate(other, (x, y) -> x - y); } /** * 行列とスカラーの乗算を行うメソッド * @param scalar 乗算するスカラー値 * @return 乗算結果の行列 */ public Matrix scalarMultiply(double scalar) { Matrix result = new Matrix(rows, cols); for (int i = 0; i < data.length; i++) { result.data[i] = this.data[i] * scalar; } return result; } /** * 行列の比較を行うメソッド * @param other 比較する行列 * @return 行列が等しい場合はtrue、そうでない場合はfalse */ public boolean equals(Matrix other) { if (this.rows != other.rows || this.cols != other.cols) { return false; } for (int i = 0; i < data.length; i++) { if (this.data[i] != other.data[i]) { return false; } } return true; } /** * 行列の文字列表現を返すメソッド * @return 行列の文字列表現 */ @Override public String toString() { StringBuilder sb = new StringBuilder(); for (int i = 0; i < data.length; i++) { sb.append(data[i]); if ((i + 1) % cols == 0) { sb.append("\n"); } else { sb.append(", "); } } return sb.toString(); } /** * 行列演算を行うメソッド * @param other 演算対象の行列 * @param operator 演算子 * @return 演算結果の行列 */ private Matrix operate(Matrix other, BinaryOperator<Double> operator) { if (rows != other.rows || cols != other.cols) { throw new IllegalArgumentException("Matrices must have the same dimensions"); } Matrix result = new Matrix(rows, cols); Iterator<Double> thisIterator = iterator(); Iterator<Double> otherIterator = other.iterator(); for (int i = 0; i < data.length; i++) { result.data[i] = operator.apply(thisIterator.next(), otherIterator.next()); } return result; } /** * 行列の要素にアクセスするイテレータを返すメソッド * @return 行列の要素にアクセスするイテレータ */ private Iterator<Double> iterator() { return new Iterator<Double>() { private int index = 0; @Override public boolean hasNext() { return index < data.length; } @Override public Double next() { if (!hasNext()) { throw new NoSuchElementException(); } return data[index++]; } }; } } /** * メインクラス */ public class Main { /** * メインメソッド * @param args コマンドライン引数 */ public static void main(String[] args) { Matrix a = new Matrix(new double[][]{{1, 2, 3}, {4, 5, 6}}); Matrix b = new Matrix(new double[][]{{7, 8, 9}, {10, 11, 12}}); // 加算 Matrix sum = a.add(b); System.out.println("Sum:"); System.out.println(sum); // 減算 Matrix difference = a.subtract(b); System.out.println("Difference:"); System.out.println(difference); // スカラー倍 Matrix scaled = a.scalarMultiply(2); System.out.println("Scaled:"); System.out.println(scaled); // 比較 System.out.println("Are matrices equal? " + a.equals(b)); } }
- 実行結果
Sum: 8.0, 10.0, 12.0 14.0, 16.0, 18.0 Difference: -6.0, -6.0, -6.0 -6.0, -6.0, -6.0 Scaled: 2.0, 4.0, 6.0 8.0, 10.0, 12.0 Are matrices equal? false