Java超|オブジェクト共有のしくみ

オブジェクトが増えたように見えても、実は増えていないことがあります。参照のしくみが分かると、Javaのクラス型変数の正体がぐっとクリアに見えてきます。

ここまでJavaを学んできて、クラスからオブジェクトを作り、それを変数で扱う流れにはかなり慣れてきたと思います。

ただ、このあたりで多くの人が一度ひっかかるポイントがあります。

それは、クラス型変数に別の変数を代入したとき、オブジェクト本体はコピーされているのか、それとも別のことが起きているのか、という点です。

この疑問にしっかり答えられるようになると、Javaのオブジェクト指向はかなり理解しやすくなります。

ドラゴンボールの世界観でたとえると、戦士そのものと、その戦士を指し示すスカウター登録札は別物です。

たとえば悟空という戦士がいたとして、変数は悟空本人ではありません。
変数は、悟空を指している登録札のようなものです。

この札を別の変数へ渡したとき、悟空がもう1人増えるわけではありません。
同じ悟空を、別の札でも見られるようになるだけです。

この感覚が、参照のしくみです。

この記事では、オブジェクト共有のしくみを、ドラゴンボール風の戦士管理のイメージでやさしく整理していきます。特に、クラス型変数が何を持っているのか、代入で何が起きるのか、片方を変更するともう片方にどう見えるのか、null は何を意味するのか、といったポイントを丁寧に見ていきます。

クラス型変数は戦士そのものではない

まず最初に、ここをはっきり押さえておきたいです。

クラス型の変数は、オブジェクトそのものを入れているわけではありません。
その変数が持っているのは、どのオブジェクトを指しているかという情報です。

ドラゴンボールの世界観で整理すると、次のようになります。

Javaの概念ドラゴンボール風のたとえ
クラス戦士の設計図
オブジェクト実際に生まれた戦士
クラス型変数その戦士を指し示す登録札

つまり、変数は実体そのものではなく、実体への手がかりを持っているということです。

この違いは本当に大切です。
ここがあいまいなままだと、この先に学ぶ配列、コレクション、メソッドの引数、オブジェクトの受け渡しなどで混乱しやすくなります。

宣言しただけでは、まだ戦士は存在しない

クラス型変数を宣言しただけでは、まだオブジェクトは作られていません。

たとえば、次のような宣言を考えてみます。

SaiyanWarrior goku;

この時点で作られているのは、悟空を指すための札だけです。
まだ、実際の戦士オブジェクトは作られていません。

実際にオブジェクトが作られるのは、new を使ったときです。

goku = new SaiyanWarrior();

この流れを表で整理すると、こうなります。

処理起きていること
変数を宣言する戦士を指すための札を用意する
new で生成する実際の戦士を生み出す
変数へ代入する札がその戦士を指すようになる

この流れを意識すると、変数とオブジェクトを別物として見やすくなります。

まずは1つのオブジェクトを2つの変数で扱う

ここからは、実際のコードで見ていきましょう。
最初は、1つのオブジェクトを2つの変数で扱う例です。

ファイル名:Sample6.java

class SaiyanWarrior
{
    private int battlePower;
    private double kiPower;

    public SaiyanWarrior()
    {
        battlePower = 0;
        kiPower = 0.0;
        System.out.println("戦士を生成しました。");
    }

    public void setWarrior(int b, double k)
    {
        battlePower = b;
        kiPower = k;
        System.out.println("戦闘力を" + battlePower + "、気の力を" + kiPower + "に設定しました。");
    }

    public void show()
    {
        System.out.println("戦闘力は" + battlePower + "です。");
        System.out.println("気の力は" + kiPower + "です。");
    }
}

class Sample6
{
    public static void main(String[] args)
    {
        SaiyanWarrior goku;
        System.out.println("gokuを宣言しました。");

        goku = new SaiyanWarrior();
        goku.setWarrior(9000, 5000.0);

        SaiyanWarrior vegeta;
        System.out.println("vegetaを宣言しました。");

        vegeta = goku;
        System.out.println("vegetaにgokuを代入しました。");

        System.out.print("gokuがさす");
        goku.show();

        System.out.print("vegetaがさす");
        vegeta.show();
    }
}

このプログラムで特に大切なのは、次の部分です。

vegeta = goku;

ここで起きていることは、戦士のコピーではありません。
goku が指していた戦士を、vegeta も指すようになっただけです。

状態を表にすると、こうなります。

変数指している先
goku1人の SaiyanWarrior オブジェクト
vegeta同じ SaiyanWarrior オブジェクト

ここで大事なのは、変数名が2つあるからといって、戦士オブジェクトが2つあるわけではない、ということです。

goku と vegeta という2つの変数はあります。
でも、その2つが見ている先は同じ1つの戦士です。

ドラゴンボール風にいえば、1人の戦士に対して、登録札が2枚ついている状態です。
札が2枚あるからといって、戦士が2人に増えたわけではありません。

どんな順番で参照が共有されるのか

この流れを順番に整理すると、かなり見やすくなります。

順番処理起きていること
1SaiyanWarrior goku;goku という札を用意する
2goku = new SaiyanWarrior();新しい戦士を生成し、goku が指す
3goku.setWarrior(...);その戦士の状態を設定する
4SaiyanWarrior vegeta;vegeta という別の札を用意する
5vegeta = goku;vegeta も同じ戦士を指す
6goku.show();同じ戦士の状態を表示する
7vegeta.show();やはり同じ戦士の状態を表示する

ここから分かるのは、クラス型変数の代入は、オブジェクト本体を丸ごと複製する操作ではないということです。

代入で渡されるのは、オブジェクトそのものではなく、オブジェクトを指している参照です。

図:変数はオブジェクトそのものではなく参照

この図が示していること

この図では、goku と vegeta という2つの変数が、同じ1つの SaiyanWarrior オブジェクトを指していることを表しています。

ここから分かるのは、変数はオブジェクトそのものではなく、オブジェクトへの参照を持っているということです。
変数が2つあるからといって、オブジェクトが2つあるとは限りません。

参照先が同じなら、扱っている実体も同じです。
これが、クラス型変数の代入で最も大切なポイントです。

参照の代入はコピーではない

初心者のうちは、次のように考えやすいです。

vegeta = goku と書いたら、goku の中身がそのまま複製されて、新しい戦士オブジェクトができるのではないか。

でも、クラス型変数ではそうなりません。

代入で起きるのは、参照のコピーです。
つまり、どのオブジェクトを指しているかという情報がコピーされるだけです。

この違いを表で整理すると、こうなります。

イメージ実際に起きていること
オブジェクト本体が複製されるそうではない
参照先の情報が渡されるこれが正しい
別々のオブジェクトになるならない
同じオブジェクトを共有するそうなる

この感覚は、Javaではとても重要です。
クラス型変数は、値型の変数とは扱いが違います。

たとえば int 型の変数なら、値そのものが代入されます。
でも、クラス型変数では、実体ではなく参照が代入されます。

片方から状態を変更すると、もう片方にも影響する

次に見たいのが、同じオブジェクトを2つの変数が指しているとき、片方から状態を変更するとどうなるかです。

ファイル名:Sample7.java

class SaiyanWarrior
{
    private int battlePower;
    private double kiPower;

    public SaiyanWarrior()
    {
        battlePower = 0;
        kiPower = 0.0;
        System.out.println("戦士を生成しました。");
    }

    public void setWarrior(int b, double k)
    {
        battlePower = b;
        kiPower = k;
        System.out.println("戦闘力を" + battlePower + "、気の力を" + kiPower + "に設定しました。");
    }

    public void show()
    {
        System.out.println("戦闘力は" + battlePower + "です。");
        System.out.println("気の力は" + kiPower + "です。");
    }
}

class Sample7
{
    public static void main(String[] args)
    {
        SaiyanWarrior goku = new SaiyanWarrior();
        goku.setWarrior(9000, 5000.0);

        SaiyanWarrior vegeta = goku;

        System.out.println("悟空の状態を変更します。");

        goku.setWarrior(12000, 8000.0);

        System.out.print("gokuがさす");
        goku.show();

        System.out.print("vegetaがさす");
        vegeta.show();
    }
}

このプログラムでは、goku と vegeta が同じオブジェクトを指している状態で、goku から setWarrior() を呼び出しています。

すると、表示結果では vegeta から見た状態も変わります。

なぜかというと、両方が見ている先が同じだからです。

変化の流れを表にすると、こうなります。

タイミングgoku が指す状態vegeta が指す状態
代入直後戦闘力 9000 / 気の力 5000.0同じ
goku.setWarrior(12000, 8000.0) の後戦闘力 12000 / 気の力 8000.0同じ

ここで注目したいのは、goku の中身だけが変わったのではないということです。

そもそも goku と vegeta は、それぞれ別の戦士を持っているわけではありません。
同じ1人の戦士を見ているので、その戦士が変われば、どちらから見ても結果は同じになります。

ドラゴンボール風にたとえると、1人の戦士の訓練記録を2枚の登録札が指している状態です。
その訓練記録を書き換えたら、どちらの札から見ても新しい内容が見える、というわけです。

なぜ両方に同じ変化が見えるのか

この現象を言い換えると、変数ごとに状態を持っているのではなく、状態を持っているのはオブジェクト本体だということです。

何が状態を持っているか答え
goku という変数状態そのものは持たない
vegeta という変数状態そのものは持たない
SaiyanWarrior オブジェクト状態を持っている

つまり、変数はあくまで入り口です。
本当のデータは、参照先のオブジェクト側にあります。

そのため、どの変数から入っても、同じオブジェクトを見ているなら、見える内容は同じになります。

図:片方から変更すると両方に影響する

この図が示していること

この図では、goku と vegeta が同じオブジェクトを指している状態で、goku から状態変更を行った結果、その変化が vegeta 側にも見えることを表しています。

ここから分かるのは、参照先が同じである限り、オブジェクトの状態は共有されるということです。
変数ごとに別々の状態を持っているわけではありません。

そのため、片方から変更した内容は、もう片方から見ても同じように確認できます。

null は誰も指していない状態

参照のしくみを理解するうえで、もうひとつ大切なのが null です。

null は、何も参照していない状態を表します。

ドラゴンボール風にたとえると、戦士を指していた登録札のひもが外れて、もう誰も指していない状態です。

状態の意味を表で見ると、こうなります。

状態意味
変数がオブジェクトを参照しているどの戦士を指すか決まっている
変数が null である誰も指していない
null の変数からメソッドを呼ぶ参照先がないので問題が起きる

ここで大切なのは、null は空っぽのオブジェクトではないということです。
何も指していない、という状態そのものです。

このあたりはよく誤解しやすいところです。
null という特別なオブジェクトがあるわけではありません。

参照がなくなると、オブジェクトはどうなるか

もし、あるオブジェクトをどの変数も参照しなくなったら、そのオブジェクトには到達できなくなります。

Javaでは、このような不要になったオブジェクトを自動で片づける仕組みがあります。
これがガーベッジコレクションです。

ドラゴンボール風にたとえると、どの登録札からもたどれなくなった戦士記録が、本部によって整理対象になるようなイメージです。

表で整理すると、こうなります。

状況結果
少なくとも1つの変数が参照しているまだ使える
どの変数も参照していない不要なオブジェクトになる
不要になったオブジェクトJavaが自動的に回収対象にする

ここで大切なのは、Javaではプログラマが毎回手作業で削除しなくてもよい、という点です。

もちろん、いつ回収されるかはJavaの仕組みに任されます。
でも、不要なオブジェクトを自動的に整理してくれるのは、とても助かる仕組みです。

図:null とガーベッジコレクション

この図が示していること

この図では、goku と vegeta の両方が null になっていて、どちらの変数も戦士オブジェクトを指していない状態を表しています。

ここから分かるのは、null は何も参照していない状態だということです。
また、どの変数からも参照されなくなったオブジェクトは、不要なオブジェクトとして回収対象になることも分かります。

null は空のオブジェクトではなく、参照先がないという意味です。
この違いを意識すると、参照型の理解がかなり深まります。

値型の代入との違い

参照をよりはっきり理解するために、値型との違いも見ておくと分かりやすいです。

項目int などの値型クラス型変数
代入されるもの値そのもの参照
代入後の関係別々の値として扱われる同じオブジェクトを指すことがある
一方を変更したときもう一方には影響しない同じ参照先なら影響する

たとえば int 型なら、a = b をしたあとで a を変えても、b は変わりません。

でもクラス型変数では、a と b が同じオブジェクトを指しているなら、そのオブジェクトの状態変更は両方に見えます。

ここが、参照型の大きな特徴です。

参照のしくみが分かると、この先の見え方が変わる

この参照の考え方が分かると、Javaのクラス型変数に対する見方がかなり変わります。

これまで、変数にオブジェクトを入れている、とふんわり理解していたものが、変数にはオブジェクトそのものではなく、オブジェクトへの参照が入っている、とかなりはっきり言えるようになります。

その結果、次のようなことが理解しやすくなります。

理解しやすくなること理由
配列の中にオブジェクトを入れるしくみ配列には参照が並ぶから
メソッドへオブジェクトを渡す感覚参照が渡されるから
複数の変数で同じ実体を扱う状況参照先を共有できるから
オブジェクトのコピーが難しく感じる理由単純代入では本体は複製されないから

特に、これから配列やコレクションを学ぶときには、この感覚がとても役立ちます。
オブジェクトを扱う世界では、実体 と 参照 を分けて考えることが本当に大切です。

ここで押さえておきたい重要ポイント

最後に、このテーマで特に大切なポイントを整理しておきます。

ポイント内容
クラス型変数はオブジェクトそのものではないオブジェクトを指す参照を持つ
変数の代入で起きるのは本体のコピーではない参照先の共有が起きる
複数の変数が同じオブジェクトを指すことがあるそのとき実体は1つだけ
片方から状態を変えるともう片方にも見える参照先が同じだから
null は参照先がない状態空のオブジェクトではない
参照されなくなったオブジェクトは回収対象になるJavaが自動で管理する

ドラゴンボール風にたとえるなら、変数は戦士本人ではなく、その戦士を指し示す登録札です。

札を別の変数へ渡しても、戦士が増えるわけではありません。
同じ戦士を別の札でも見られるようになるだけです。

この感覚をしっかり持てると、Javaのオブジェクト指向はぐっと理解しやすくなります。

クラス型変数の正体が見えてくると、この先の学習でも、なぜそうなるのかが自然に分かる場面が増えてきます。

参照は、Javaを学ぶうえで本当に大切な土台です。
今のうちに、変数は実体ではなく参照を持っている、という感覚をしっかり身につけておくと、この先の理解がかなり楽になります。