Javaにおいて、クラスは重要な概念の一つだ。クラスを理解することは、Javaプログラミングの基礎を身につける上で不可欠である。まずは、クラスの本質的な概念から理解していこう。
クラスとは何か
クラスとは、オブジェクトの設計図である。
より具体的に説明すると、プログラム内で扱いたい対象(モノや概念)の特徴(属性)と、その対象が行う操作(振る舞い)をまとめて定義したものである。
例えば、「車」というクラスを考えた場合、車の色や型式などの属性と、「走る」「止まる」などの振る舞いを一つの単位として定義することができる。
このようにクラスは、プログラムの中で扱う対象を抽象化し、再利用可能な形で表現する仕組みである。
クラスを使用する目的
クラスを使用する主な目的は、プログラムの構造化と再利用性の向上である。
クラスを用いることで、関連する機能やデータを一つのまとまりとして扱うことができ、プログラムの見通しが良くなる。
また、一度作成したクラスは他のプログラムでも再利用することが可能であり、開発効率が向上し、保守性も高まる。
クラスを使用することで、実世界の概念をプログラムとして自然に表現することができ、より直感的なプログラム設計も可能となる。
オブジェクト指向プログラミングにおけるクラスの役割
オブジェクト指向プログラミングにおいて、クラスは中心的な役割を担っている。
クラスは、データ(属性)と、そのデータを操作するための手続き(メソッド)をカプセル化する機能。(※全く同じ動作をするときにいちいち人間が思い出さずとも勝手に手が動くアレと同じイメージ・それをプログラミングではclassとして名前をつけていつでも呼び出せるように保存するイメージ・ポケモンの必殺技(メソッド)、種類(属性)、レベル(属性)も含めて保存できる感じ)
プログラムの各部分を独立した部品として扱うことができ、プログラムの複雑さを管理しやすくなる。また、クラスは継承という仕組みを通じて、既存のクラスの機能を拡張したり、特殊化したりすることができる。
このような特徴により、クラスはオブジェクト指向プログラミングの基盤として、プログラムの柔軟性と拡張性を支えている。
さらに、クラスを介したポリモーフィズム(多態性)の実現により、プログラムの抽象度を高め、より柔軟な設計が可能となる。
クラスの基本構造
クラスの基本概念を理解したところで、実際のクラスがどのように構成されているのかを詳しく見ていく。
Javaのクラスは、明確な構造規則に従って記述する必要がある。
クラスの定義方法について
// Studentクラスの定義
// ↓publicは他のクラスからもアクセスできる
public class Student {
// クラスはStudentで、
// この中にフィールドやメソッドを書いていく
}
Javaでクラスを定義する際は、classを使用する。
クラス名は基本必ず大文字で始まり、ファイル名と一致させる必要がある。(※必ずしも大文字・ファイル名である必要はないが、Javaのコーディング規約ではそうなっている、ファイル名と一致させる方が良いが、わかりやすい名前であれば自分のためにOK)
クラスの定義には、アクセス修飾子(public, protected, private)を付けることができる。
publicクラスの場合、そのクラス名と同じ名前のソースファイル(.java)に保存しなければならない。
クラスの本体は波括弧{}で囲まれ、その中にフィールドやメソッドを記述する。
フィールド(メンバ変数)の宣言について
フィールドとは、クラスが持つデータを格納する変数である。
public class Student {
private String name; // 生徒の名前を保存するフィールド
private int age; // 生徒の年齢を保存するフィールド
}
フィールドはクラスの特性や状態を表現するために使用される。
フィールドの宣言には、アクセス修飾子、静的(static)宣言、最終(final)宣言などを付けることができる。(※シリアライズ制御用のtransient修飾子や、スレッドセーフ性に関わるvolatile修飾子なども必要に応じて使用可能)
フィールドの型は、プリミティブ型(int, doubleなど)やクラス型を指定することができる。これら型の並列を使用することも可能である。
また、フィールドには初期値を設定することができ、設定しない場合は型に応じたデフォルト値が自動的に設定される。
メソッドの定義
メソッドは、クラスが持つ機能や振る舞いを定義する。
public class Student {
private String name;
// 名前を取得するメソッドの定義
public String getName() {
return name;
}
// 名前を設定するメソッドの定義
public void setName(String name) {
this.name = name;
}
}
メソッドの定義には、戻り値の型、メソッド名、引数リストを指定する。
メソッド名は基本的に小文字で始めることが推奨され、本体は波括弧{}で囲まれ、その中に具体的な処理を記述する。ただ、これは言語仕様というよりコーディング規約の話である。
また、一般的にはcamelCaseを使用する。(※camelCaseは複数の単語を組み合わせて識別子を作る際の命名規則/特徴は直下に記述するので覚えておくと良いだろう。)
上述の特徴(クラス名はPascalCase/UpperCamelCase、メソッド名はcamelCase/lowerCamelCase)
壱. 最初の単語は小文字
弍. 2番目以降の単語は大文字先頭で始める
参. 単語と単語の間にスペースや記号を入れない
アクセス修飾子を付けることもでき、クラス外からのアクセス制御が可能である。
また、staticキーワードを付けることで、インスタンス化せずに呼び出せるクラスメソッドを定義することができる。
コンストラクタの役割
public class Student {
private String name;
// 引数なしのコンストラクタの定義
public Student() {
this.name = "名前なし";
}
// 引数を持つコンストラクタの定義
public Student(String name) {
this.name = name;
}
}
コンストラクタは、クラスのインスタンスを生成する際に呼び出される特殊なメソッドで、クラス名と同じ名前を持ち、戻り値の型を指定しない。
newでインスタンスが生成される際に自動的に呼び出され、インスタンスの初期状態の設定を行う。
インスタンス生成時に初期値を設定できるよう、引数を持つコンストラクタを定義することができ、明示的にコンストラクタを定義しない場合は、引数を持たないデフォルトコンストラクタが自動的に生成される。
ただし、1つでもコンストラクタを定義すると、デフォルトコンストラクタは自動生成されなくなる点には注意が必要だ。
コンストラクタの実装では、this()を使用して同じクラス内の別のコンストラクタを呼び出すことができ、コンストラクタのオーバーロードを効率的に実装できる。
また、アクセス修飾子を付けることでクラスのインスタンス化を制御することも可能である。
クラスの種類と特徴
クラスの基本構造について理解したところで、Javaにおける様々なクラスの種類とその特徴について見ていく。
最初は複雑に感じるかもしれないが、それぞれのクラスには明確な役割があり、プログラムの設計に大きな影響を与える重要な概念であるので理解しておきたい。
具象クラス
プログラミングを始めたばかりの人が最初に触れるのが、この具象クラスである。
「具象」という言葉は「具体的な」という意味で、実際に動作する処理が全て書かれているクラスのことを指す。
日常生活で例えるなら、完成された製品の設計図のようなものだ。
具象クラスの最大の特徴は、すぐに使えることである。
newを使用してインスタンス化できるため、プログラムの中ですぐに活用することができる。
また、全てのメソッドには具体的な処理が書かれているため、使い方が分かりやすいという利点もある。
public class User {
// ユーザーの情報を保存するための変数
private String name; // 名前を保存する
private int age; // 年齢を保存する
// ユーザーを作成するときの処理
public User(String name, int age) {
// this.nameは「このクラスのname変数」という意味
this.name = name;
this.age = age;
}
// ユーザーが挨拶するための処理
public void sayHello() {
// 保存されている名前を使って挨拶文を作る
System.out.println("こんにちは、" + name + "です。");
System.out.println("年齢は" + age + "歳です。");
}
}
// このクラスは以下のように使うことができる
User taro = new User("太郎", 25);
taro.sayHello(); // "こんにちは、太郎です。年齢は25歳です。" と表示される
抽象クラス
具象クラスを理解したところで、次は抽象クラスについて見ていこう。
抽象クラスは、abstractキーワードを使用して定義する特殊なクラスである。具象クラスと異なり、それ自体では不完全なクラスであり、直接インスタンス化することはできない。
抽象クラスが必要になるのは、複数のクラスで共通する機能をまとめたいけれど、一部の機能は個々のクラスで独自に実装したい場合である。
例えば、先ほどのコード例で示した動物のクラスでは、全ての動物は眠ることができるが、鳴き方は動物によって異なる。
このような場合に抽象クラスが効果的である。
// 抽象クラスの定義
public abstract class Animal {
protected String name; // 動物の名前
// すべての動物に共通する処理(眠る)
public void sleep() {
System.out.println(name + "は眠っています。zzz");
}
// 動物によって異なる処理(鳴く)
// 抽象メソッドは中身を書かない。継承先で必ず実装する必要がある
public abstract void makeSound();
}
// 犬のクラス(Animalクラスを継承)
public class Dog extends Animal {
// 犬を作成するときの処理
public Dog(String name) {
this.name = name;
}
// 犬の鳴き方を実装
@Override
public void makeSound() {
System.out.println(name + ":ワンワン!");
}
}
// 猫のクラス(Animalクラスを継承)
public class Cat extends Animal {
public Cat(String name) {
this.name = name;
}
@Override
public void makeSound() {
System.out.println(name + ":ニャー!");
}
}
// 使用例
Dog pochi = new Dog("ポチ");
pochi.makeSound(); // "ポチ:ワンワン!" と表示
pochi.sleep(); // "ポチは眠っています。zzz" と表示
Cat tama = new Cat("タマ");
tama.makeSound(); // "タマ:ニャー!" と表示
tama.sleep(); // "タマは眠っています。zzz" と表示
インターフェース
インターフェースは、クラスが実装すべきメソッドを定義する仕組みで、interfaceキーワードを使用して定義し、クラスが「どのような機能を持つべきか」という契約書のような役割を行う。
抽象クラスと異なり、インターフェースはメソッドの実装を持たず、多重継承が可能である。
また、インターフェースが特に有用なのは、クラスの設計段階においてであり、プログラムの設計者は、インターフェースを通じてクラスが持つべき機能を明確に定義できる。
さらにJavaでは1つのクラスが複数のインターフェースを実装できるため、柔軟な設計が可能となる。
// 図形が持つべき機能を定義するインターフェース
public interface Shape {
// 図形を描画する
void draw();
// 図形の面積を計算する
double getArea();
}
// 円を表すクラス
public class Circle implements Shape {
private double radius; // 半径
public Circle(double radius) {
this.radius = radius;
}
@Override
public void draw() {
System.out.println("○を描画します。");
}
@Override
public double getArea() {
return Math.PI * radius * radius; // 円の面積を計算
}
}
// 四角形を表すクラス
public class Rectangle implements Shape {
private double width; // 幅
private double height; // 高さ
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public void draw() {
System.out.println("□を描画します。");
}
@Override
public double getArea() {
return width * height; // 四角形の面積を計算
}
}
内部クラス
最後に、特殊な形態である内部クラスについて見ていこう。
内部クラスは、クラスの中に定義された別のクラスである。
一見すると複雑に見えるかもしれないが、実際には非常に実用的な機能である。
内部クラスの主な用途は、外部からは見せたくない処理を隠蔽することである。
また、特定のクラスでのみ使用する機能をカプセル化する際にも便利だ。
例えば、本のクラスの中にしおりを表す内部クラスを定義することで、しおりの機能を本に関連付けて管理することができるような形だ。
public class OuterClass {
private int value = 10;
// 非staticな内部クラス
public class InnerClass {
public void printValue() {
System.out.println(value); // 外部クラスのメンバにアクセス可能
}
}
// staticな内部クラス
public static class StaticInnerClass {
public void doSomething() {
System.out.println("Static内部クラスの処理");
}
}
}
これらのクラスの種類は、それぞれが異なる目的と特徴を持っている。初学者にとっては一度に全てを理解するのは難しいかもしれないが、プログラミングの経験を積むにつれて、それぞれの使い所が自然と分かってくるだろう。
大切なのは、各クラスの特徴を理解し、目的に応じて適切なクラスを選択できるようになることである。
次は、これらクラスを実際にどのように使用するのかについて見ていこう。
クラスの使用方法
これまでクラスの基本概念、構造、種類について述べてきたが、クラスの理解をこなすためには、実際にどのように使用するのかを知ることが重要である。
ここでは、クラスの具体的な使用方法について、実践的な観点から解説していく。
インスタンスの生成
クラスからオブジェクトを作成することを、インスタンス化と呼ぶ。
インスタンス化は、newを使用して行う。
このとき、コンストラクタが呼び出され、オブジェクトの初期化が行われる。
public class Person {
private String name;
private int age;
// コンストラクタ
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// インスタンスの生成
Person person = new Person("山田太郎", 25);
インスタンス化の際に注意すべき点として、コンストラクタの引数の型と順序がある。
これは定義されたコンストラクタと完全に一致している必要がある。
また、生成されたインスタンスは変数に代入して保持する必要があり、この変数の型はクラス型となる。
メソッドの呼び出し
インスタンス化したオブジェクトのメソッドは、ドット演算子(.)を使用して呼び出すことができる。
メソッドの呼び出しには、インスタンスメソッドとクラスメソッド(staticメソッド)の2種類がある。
public class Calculator {
// インスタンスメソッド
public int add(int a, int b) {
return a + b;
}
// クラスメソッド(staticメソッド)
public static int multiply(int a, int b) {
return a * b;
}
}
Calculator calc = new Calculator();
int sum = calc.add(5, 3); // インスタンスメソッドの呼び出し
int product = Calculator.multiply(4, 2); // クラスメソッドの呼び出し
継承の活用方法
継承は、既存のクラスの機能を拡張または変更する強力な機能であり、extendsを使用して実現する。
継承を使用することで、コードの再利用性が高まり、保守性も向上する。
public class Vehicle {
protected String brand;
public void start() {
System.out.println("エンジンを始動します");
}
}
public class Car extends Vehicle {
private int doors;
public Car(String brand, int doors) {
this.brand = brand;
this.doors = doors;
}
@Override
public void start() {
super.start(); // 親クラスのメソッドを呼び出し
System.out.println("空調システムを起動します");
}
}
カプセル化の実装
カプセル化は、クラスの内部データを外部から直接アクセスできないように保護する機能である。
これはアクセス修飾子とgetter/setterメソッドを組み合わせて実現する。
public class BankAccount {
private double balance; // privateで外部からのアクセスを制限
public BankAccount(double initialBalance) {
this.balance = initialBalance;
}
// getter
public double getBalance() {
return balance;
}
// 入金メソッド
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
// 出金メソッド
public boolean withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
return true;
}
return false;
}
}
カプセル化により、クラスの利用者は内部実装を知る必要がなく、公開されたメソッドを通じて安全にデータを操作することができる。
また、将来的な実装の変更にも柔軟に対応できるという利点がある。
クラス設計のベストプラクティス
これまでクラスの基本から使用方法まで見てきた。
しかし、実践的なJavaプログラミングにおいては、クラスを適切に設計することが重要である。
ここでは、最後にクラス設計における重要な原則とベストプラクティスについて解説する。
命名規則とコーディング規約
Javaにおける命名規則は、コードの可読性と保守性を高めるために重要である。
クラス名は必ず大文字で始まるアッパーキャメルケースを使用する。
また、メソッドやフィールド名は小文字で始まるローワーキャメルケースを使用する。
public class BankAccount { // クラス名は大文字で始まる
private double currentBalance; // フィールド名は小文字で始まる
public double getAccountBalance() { // メソッド名は小文字で始まる
return currentBalance;
}
}
定数は全て大文字で、単語間はアンダースコアで区切る。
また、一時的な変数名には意味のある名前を付けることが重要である。
public static final int MAX_RETRY_COUNT = 3; // 定数は全て大文字
public void processUserData(String userData) { // 意味のある変数名
int retryCount = 0; // 説明的な変数名
}
適切なアクセス修飾子の選択
アクセス修飾子は、クラスのカプセル化を実現するための重要な要素である。
フィールドは原則としてprivateとし、必要に応じてgetterやsetterを呼ぶ。
メソッドは、外部から利用する必要があるもののみpublicとする。
public class Employee {
private String name; // フィールドはprivate
protected int salary; // 継承先からのみアクセス可能
public String getName() { // 公開するメソッドはpublic
return name;
}
private void calculateBonus() { // 内部でのみ使用するメソッドはprivate
// 処理内容
}
}
クラスの責務と単一責任の原則
クラスは1つの明確な責務のみを持つべきである。
これを単一責任の原則(Single Responsibility Principle)と呼ぶ。
複数の責務を持つクラスは、それぞれの責務ごとに分割するべきである。
// 良い例:1つの責務に集中したクラス
public class UserAuthentication {
public boolean authenticate(String username, String password) {
// 認証処理のみを担当
return checkCredentials(username, password);
}
}
// 悪い例:複数の責務を持つクラス
public class UserManager {
public boolean authenticate(String username, String password) { }
public void sendEmail(String message) { } // 認証と関係のない処理
public void updateDatabase() { } // 認証と関係のない処理
}
以上の原則に従うことで、保守性が高く、理解しやすいクラス設計を実現することができる。
また、将来的な機能追加や変更にも柔軟に対応できるコードとなる。
クラス設計は、プログラムの品質を大きく左右する重要な要素であることを常に意識する必要がある。
以上。