はじめに
自己紹介
皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、約30年にわたって企業システムの開発に携わってきました。現在も現役のアーキテクトとして、重要システムの技術設計やソリューションの選定を担当しています。直近では、システム開発へのAI適用(AI駆動開発)という全社的な施策でリーダーを務めています。
セミナーでの登壇や雑誌への技術記事の寄稿を行ってきたほか、以下の書籍も執筆しています。
- 「マスタリング Java EE 5」(翔泳社、2007年)
- 「アプリケーションアーキテクチャ設計パターン」(技術評論社、2017年)
- 「Javaエンジニアのためのソフトウェアテスト実践入門」(技術評論社、2024年)
Udemy講座のご紹介
この記事の内容は、私が講師を務めるUdemy講座『新 Java Basic編 / AI対応版』の一部の範囲をカバーしたものです。『新 Java Basic編 / AI対応版』はこちらのリンクから購入できます。
この講座は、以下のような皆様にお薦めします。
- AIが書いたJavaのコードを読んで、正しさと設計の良し悪しを判断できるようになりたい方
- 新人のシステムエンジニアとして、Javaの基礎を仕組みから身に付けたい方
- AIを前提とした開発へ学び直し(リスキリング)をしたい方
- 「Oracle認定Javaプログラマ」の取得を目指し、Javaの基礎を固めたい方
AIとチャットできるオールインワンの学習環境をリリースしました
Udemy講座の演習を動かすための学習環境として、Windows向けのデスクトップアプリ「Codinable(コーディナブル)」をリリースしました。
エディタ、実行結果、テスト結果、SQL、Webブラウザ、そしてAIチャットを1つの画面にまとめた、オールインワンの学習環境です。
学習に必要なものは、すべてCodinableの中で完結します。
- 演習の実行環境(Java 25、Gradle、Bashなど)が一式同梱されており、JDKのインストールや環境変数の設定は不要です
- インストールして「新規コース開始・再開」からコースを選ぶだけで、講座のすべての演習がそのまま動きます
- 書き換えて動かなくなっても、「初期化」で配布時の状態に戻せます
- 開いている演習のファイルと実行結果を踏まえて、AIに質問したり(Askモード)、ファイルの書き換えを指示したり(Agentモード)できます
AIに書き換えを指示し、差分を確かめてから実行する、というAI駆動開発の基本の流れを、Codinableの中だけで安全に体験できます。
AIチャットは、受講者ご自身が生成AIサービス(Anthropic、OpenAI、Googleのいずれか)のAPIキーを登録して使うBYOK(Bring Your Own Key)方式です。利用料金は各社から直接請求されます。APIキーを登録しなくても、AIチャット以外の機能はすべて使えます。
Codinableのインストーラは、以下のGitHubのReleasesページからダウンロードできます(64ビット版のWindows 10/11、管理者権限は不要)。
この記事を含むシリーズ全体像
この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。
7.2 クラスの主要なメンバー
チャプターの概要
チャプター7.1では、クラスとインスタンスの関係と、newによるインスタンス生成の手順を取り上げました。本チャプターでは、クラスの主要なメンバーであるフィールド、メソッド、コンストラクタを、「インスタンスの状態がどこで決まり、どこで変わるか」という観点から整理します。AIが書いたクラスをレビューするときは、フィールドの初期化漏れ、引数とフィールドの取り違え、意図しない状態の変更が、典型的な確認ポイントになります。
7.2.1 フィールドとメソッド
本レッスンでは、フィールドがインスタンスの状態を保持し、メソッドが引数と戻り値で呼び出し元とやり取りする仕組みを理解し、その受け渡しが正しいかを判断できるようになることを目指します。
フィールドはインスタンスの状態
フィールドは、インスタンスが持つ属性や状態を表す変数で、メソッドの中のローカル変数と対比してメンバー変数とも呼ばれます。フィールドには、インスタンスを参照する変数を起点に変数名.フィールド名でアクセスします。フィールドだけを持つPerson2クラスと、その使い方を以下に示します。
- 07_class_instance: Person2
class Person2 {
String name;
int age;
}
- 07_class_instance: Main_7_2
Person2 p2 = new Person2();
p2.name = "Alice"; //【1】
p2.age = 25;
System.out.println("name => " + p2.name);
- 【1】変数
p2を起点に、各フィールドへ値を代入しています - 代入する前のフィールドは、既定値(参照型は
null、intは0、booleanはfalse)になっています
インスタンスの生成直後と、フィールドへの代入後の状態を図で比べます。
- 左は
new Person2()の直後で、ヒープ上のPerson2インスタンスのnameがnull、ageが0である - 右は
p2.name = "Alice"とp2.age = 25の後で、同じインスタンスのnameが"Alice"、ageが25である - どちらも、スタックの変数
p2が同じインスタンスを参照している - 左の状態で
nameを使うと、nullのまま処理が進むことを示す注意のラベルが付く
外部からフィールドを直接書き換えられると、どこで状態が変わったかを追えなくなります。フィールドへのアクセスを制限する修飾子と、その設計はチャプター10.1で取り上げます。
【表】メンバーに付ける主な修飾子
| 修飾子 | 付ける対象 | 意味 | 参照先 |
|---|---|---|---|
public/protected/private
|
フィールド・メソッド・コンストラクタ | アクセスできる範囲を決める | チャプター10.1 |
static |
フィールド・メソッド | インスタンスではなくクラスに属する | チャプター8.1 |
final |
フィールド・メソッド | フィールドは再代入の禁止、メソッドはオーバーライドの禁止 | チャプター8.2、チャプター11.1 |
abstract |
メソッド | 処理を持たない抽象メソッドである | チャプター11.1 |
synchronized |
メソッド | マルチスレッド環境で同期化する | Udemyコース「新・Java Advanced編・AI対応版」 |
メソッドの呼び出しと引数の対応
メソッドは、戻り値型 メソッド名(型 引数名, ...) { 処理 }の形で宣言する、クラスの振る舞いです。戻り値を返さない場合は戻り値型にvoidを指定します。別のクラスからは変数名.メソッド名(引数, ...)、同じクラスの中からはメソッド名(引数, ...)で呼び出します。指定した回数だけ文字列を表示するPrinterクラスと、その呼び出しを以下に示します。
- 07_class_instance: Printer
public class Printer {
void printMessage(String message, int count) {
for (int i = 0; i < count; i++) {
System.out.println(message);
}
}
}
- 07_class_instance: Main_7_2
Printer printer = new Printer();
printer.printMessage("Hello, Alice!", 5);
printer.printMessage("Goodbye, Bob!", 10);
- 受け取った引数は、メソッドの中ではローカル変数として扱われます
- 繰り返しの処理は
printMessage()の中に1回だけ書かれ、引数を変えるだけで2つの仕様を実現しています
呼び出し元と呼び出し先の引数の対応を、図で整理します。
- 呼び出し元のコードは
printMessage("Hello, Alice!", 5);である - 第一引数の
"Hello, Alice!"は、呼び出し先のメソッド宣言のString messageに渡される - 第二引数の
5は、int countに渡される - 引数は順番どおりに対応し、型が一致しないとコンパイルエラーになる
引数は名前ではなく順番で対応します。同じ型の引数が並ぶメソッドでは、順番を取り違えてもコンパイルが通るため、レビューで注意が必要です。
複数の呼び出し元が1つのメソッドを共有する関係を、図で整理します。
- 呼び出し元①は
printMessage("Hello, Alice!", 5)、呼び出し元②はprintMessage("Goodbye, Bob!", 10)である - 2つの呼び出し元は、同じ
void printMessage(String message, int count)メソッドを呼び出す -
printMessageメソッドは、for文で表示を繰り返す - 同じ処理を1つのメソッドにまとめることで、処理を共通化できる
AIは似た処理を複数の場所に書き写すことがあります。同じようなfor文が何か所にも現れたら、メソッドに共通化するよう指示します。
戻り値とreturn文
メソッドの処理結果は、return 値;で呼び出し元へ返します。returnに到達するとメソッドは終了し、その後ろに到達しえない文を書くとコンパイルエラーになります。足し算を行うCalculatorクラスと、その呼び出しを以下に示します。
- 07_class_instance: Calculator
class Calculator {
int add(int x, int y) {
int result = x + y;
return result;
}
// subtract()メソッドは省略
}
- 07_class_instance: Main_Calculator
Calculator calc = new Calculator();
int answer = calc.add(30, 10);
System.out.println(answer); // 40と表示される
- 戻り値型
intと、returnで返す値の型、呼び出し元で受け取る変数の型が一致している必要があります - 戻り値型が
voidのメソッドでも、return;と書いて途中で処理を終えることができます
引数と戻り値がどのように受け渡されるかを図で整理します。
- 呼び出し元のコードは
int answer = calc.add(30, 10);である - 第一引数の30は呼び出し先の
xに、第二引数の10はyに渡される - 呼び出し先の
addメソッドは、x + yの結果をresultに代入する -
return result;によって、resultの値が戻り値として呼び出し元へ返り、answerに代入される
レビューの観点
- 生成直後に
nullのままのフィールドを使う流れが無いかを確かめる - 同じ型の引数が並ぶメソッドで、呼び出し側が引数の順番を取り違えていないかを確かめる
- 同じ処理が複数の場所に書き写されていないか、メソッドに共通化できないかを確かめる
- メソッド名が動詞または動詞+名詞(
createGreetMessageなど)、論理値を返すものはisXxx・hasXxx・canXxxの形で、処理の内容を表しているかを確かめる
7.2.2 スコープとthis
本レッスンでは、フィールドとローカル変数のスコープの違いを理解し、同じ名前の変数による取り違えや、意図しない状態の変更を見抜けるようになることを目指します。
フィールドとローカル変数のスコープ
ローカル変数のスコープは宣言した位置からそのブロックの終わりまで(チャプター6.1参照)ですが、フィールドのスコープはクラス全体です。
class クラス {
フィールドa;
メソッドb(....) {
ローカル変数c = xxx;
if (....) {
ローカル変数d = xxx;
}
}
}
このクラスにおける各変数のスコープを、図で整理します。
- フィールド
aのスコープはクラス全体であり、クラス内のどこからでも参照できる - ローカル変数
cのスコープは、メソッドb()の本体全体である - ローカル変数
dのスコープは、if文のブロックの中だけである
フィールドは、ローカル変数と違ってメソッドが終わっても消えず、インスタンスが存在する限り値を保持します。
同じ名前の変数とthis
フィールドとローカル変数(引数を含む)は同じ名前を付けられ、両方が見える位置ではローカル変数が優先されます。フィールドを指したいときは、自分自身のインスタンスへの参照を表すthisを使い、this.フィールド名と書きます。コンストラクタで引数をフィールドに代入する、正しいコードと誤ったコードを比べます。
Person(String name, int age) {
this.name = name; // 正しい: 引数nameをフィールドnameへ
this.age = age;
}
Person(String name, int age) {
name = name; // 誤り: 引数nameを引数name自身へ代入
this.age = age;
}
- 1つ目は、
Personクラスのコンストラクタ(07_class_instance: Person)と同じ書き方です - 2つ目は、左辺の
nameも引数を指すため、フィールドnameはnullのまま残ります。コンパイルエラーにはなりません
2つのコードで、値がどこへ代入されるかを図で比べます。
- 上段は
this.name = nameで、引数nameの"Alice"が、ヒープ上のPersonインスタンスのフィールドnameへ代入される - 上段の結果は、フィールド
nameが"Alice"になる - 下段は
name = nameで、引数nameの"Alice"が、同じ引数nameへ戻ってくるだけになる - 下段の結果は、フィールド
nameがnullのまま残り、sayHello()では「私はnull」と表示される
状態を変えるメソッドと変えないメソッド
メソッドの中からは、フィールド名を直接書いてフィールドを参照・更新できます。メソッドの呼び出しによってフィールド(状態)が変化することを、副作用と呼びます。Person3クラスの2つのメソッドを以下に示します。
- 07_class_instance: Person3
// 副作用
void increaseAge() { //【1】
age += 1;
}
// withAge()などのメソッドは省略
// boolean型を返す
boolean isAdult() { //【2】
return 20 <= age;
}
- 【1】
increaseAge()は、ageフィールドに1を加えて状態を変えます - 【2】
isAdult()は、ageフィールドを読むだけで状態を変えず、判定結果を論理値で返します
2つのメソッドが、インスタンスの状態にどう関わるかを図で比べます。
- 左は
increaseAge()の呼び出しで、Personインスタンスのageが25から26に変わる - 左には「副作用あり: 呼び出すたびに結果が変わる」というラベルが付く
- 右は
isAdult()の呼び出しで、ageの25を読んでtrueを返し、インスタンスの状態は25のまま変わらない - 右には「副作用なし: 何度呼んでも同じ結果」というラベルが付く
状態を読むだけに見える名前(isXxxやgetXxx)のメソッドが、内部でフィールドを書き換えていると、呼び出し側は変更に気付けません。
レビューの観点
- 引数とフィールドが同じ名前のとき、フィールドへの代入に
this.が付いているかを確かめる(name = nameは自己代入の誤り) -
isXxxやgetXxxのように状態を読むだけに見える名前のメソッドが、フィールドを書き換えていないかを確かめる - メソッドの中だけで使う値がフィールドにされ、インスタンスの状態が不必要に増えていないかを確かめる
7.2.3 コンストラクタ
本レッスンでは、コンストラクタがインスタンスを初期化する仕組みと、デフォルトコンストラクタの条件を理解し、不完全な状態のインスタンスを生まない設計を判断できるようになることを目指します。
newとコンストラクタ
コンストラクタは、newでインスタンスを生成するときに呼び出され、インスタンスを初期化します。名前はクラス名と同じにし、戻り値型は書きません。new クラス名(引数, ...)で渡した引数は、メソッドと同じく順番どおりにコンストラクタの引数へ対応します。Personクラスのコンストラクタで、2つのインスタンスを生成するコードを以下に示します。
- 07_class_instance: Main_7_2
Person p1 = new Person("Alice", 25); //【1】
Person p2 = new Person("Bob", 35); //【2】
- 【1】
nameが"Alice"、ageが25のインスタンスが生成されます - 【2】
nameが"Bob"、ageが35のインスタンスが生成されます
コンストラクタ呼び出しによってインスタンスが生成される流れを、図で整理します。
-
Personクラスは、name : Stringとage : intの属性と、sayHello() : voidの操作を持つ - コンストラクタ
Person(String name, int age)は、引数をthis.nameとthis.ageに代入する - 【1】
new Person("Alice", 25)によって、nameが"Alice"、ageが25のPersonインスタンスが生成される - 【2】
new Person("Bob", 35)によって、nameが"Bob"、ageが35のPersonインスタンスが生成される
デフォルトコンストラクタ
クラスにコンストラクタが1つも無い場合、コンパイラは引数を取らず何もしないコンストラクタを暗黙的に生成します。これをデフォルトコンストラクタと呼びます。Person2クラスがコンストラクタを持たないのにnew Person2()で生成できたのは、このためです。引数を取るコンストラクタを1つでも宣言すると、デフォルトコンストラクタは生成されません。
コンストラクタの宣言の有無によって、newで生成できる形がどう変わるかを図で示します。
- 左は、コンストラクタを1つも宣言していない
Person2で、コンパイラがデフォルトコンストラクタPerson2()を補い、new Person2()が成功する - 中央は、
Person(String name, int age)だけを宣言したクラスで、デフォルトコンストラクタは補われず、new Person()がコンパイルエラーになる - 右は、
Person()とPerson(String name, int age)の両方を宣言したPersonで、new Person()もnew Person("Alice", 25)も成功する
不完全なインスタンスを作らない
コンストラクタの目的は、インスタンスを使える状態に初期化することです。引数を取らないコンストラクタで生成し、後からフィールドを1つずつ埋める書き方では、途中の状態のインスタンスが生まれ、設定し忘れたフィールドがnullのまま使われる危険があります。必須の値はコンストラクタの引数で受け取るのが基本です。
2つの生成の仕方を図で比べます。
- 左は「問題のある生成」で、
new Person()の直後のインスタンスはnameがnull、ageが0であり、その後p.name = "Alice"だけが実行される - 左では、
ageの設定が漏れたまま、0歳の人物として処理が進む - 右は「良い生成」で、
new Person("Alice", 25)によって、生成された時点でnameとageの両方が決まる - 右では、名前と年齢が決まっていない
Personインスタンスが存在しない
フィールドの数が多く、コンストラクタの引数が長くなる場合の対策(Builderパターンなど)は、チャプター7.3で取り上げます。
レビューの観点
- 必須の値がコンストラクタの引数で受け取られ、生成直後から使える状態になっているかを確かめる
- 引数を取らないコンストラクタが、フレームワークの要件などの理由も無く追加され、不完全なインスタンスを作れるようになっていないかを確かめる
- 引数を取るコンストラクタを追加したことで、既存の
new クラス名()がコンパイルエラーになっていないかを確かめる - コンストラクタの中で、引数からフィールドへの代入に漏れや取り違えが無いかを確かめる
このチャプターで学んだこと
本チャプターでは、以下を取り上げました。
- フィールドはインスタンスの状態を保持し、代入されるまでは既定値(
null、0、false)であること - メソッドの引数は名前ではなく順番で対応し、戻り値は
returnで呼び出し元へ返ること - フィールドのスコープはクラス全体で、同じ名前のローカル変数があるときは
thisでフィールドを指す必要があること - 状態を変えるメソッド(副作用あり)と読むだけのメソッドを、名前と実装で区別してレビューすること
- デフォルトコンストラクタはコンストラクタが1つも無い場合にだけ生成され、必須の値はコンストラクタで受け取って不完全なインスタンスを作らないこと











