Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

「nullを代入することにより参照を切る」という動作はどのような場面で利用されるのか?メモリの様子は?

Q&A

Closed

解決したいこと

Javaの基本について再学習している者です。
質問したいことは

  • タイトルのとおり、 「nullを代入することにより参照を切る」 という動作について2点
  • String型変数は参照型変数ではないのか?(できればご教示いただけると幸いです)

の計3点あります。
以下で詳しく説明いたします。

5/10、コード内容と日本語を一部訂正しました。

「参照を切る」ということへの現時点での理解

nullについて学習をしていた際、

nullとは「何もない」という意味で、配列変数aのような参照型の変数に代入することができます。nullが代入されると、参照型の変数はどこも参照していない状態になります。

という記載が書籍1にありました。
例として以下のようなコードがあるとします。

public class Main
  public static void main (String args[]){
    int [] a = {1, 2, 3};
    int [] b = a; 
    b = null;  //参照を切った。

    System.out.println("~配列a~");
    for(int x:a){
      System.out.println(x);
    }
  }
}

この表示結果が以下のようになります。

~配列a~
1
2
3
~配列b~
Exception in thread "main" java.lang.NullPointerException: Cannot read the array length because "<local3>" is null
        at Main.main(Main.java:15)

質問1

しかし、配列の「参照を切る」というこの動作は、実際にどのような場面で利用されるのか全くイメージが湧きません。
GCがあるにも関わらず、わざわざ変数の初期化の必要がある場面はどのような時起こるのでしょうか?

質問2

2つ目の質問は参照を切られた後のメモリの様子についてです。
上記コードが動いた場合、配列aが初期化されたint[] a = {1, 2, 3};の時点で配列a用のメモリ区画が用意されたと思います。
そしてint[] b = a;の時点で、そのメモリ区画のアドレスを配列bが参照しているという状態だと思います。
では、配列bの参照が切られた後に、
新しく配列bに値が代入されたb[0] = 10;の時点で、配列b用のメモリ区画が新しく用意された
配列bにはメモリ区画が一切割り当てられていない、という認識で正しいでしょうか?
その場合この後配列bを(数値を代入するなどして)利用することは全くできないのでしょうか?

質問3

3つ目の質問は、nullStringについての質問です。
同様の書籍にnullについて以下のような記載がありました。

int型などの基本型変数には代入することができない。

ところで、nullはString型の変数に代入することができると考えています。
ということは、String型の変数は参照型変数のはずです。
しかし以下のようなコードを動かすと

public class Main{
  public static void main (String [] args) {
    String a = "あいう";
    String b = a;  //同じメモリ区画を参照してるはず?
    b = "えお";  //aとb両方のメモリ区画の内容を書き換えたはず?

    System.out.println(a);
    System.out.println(b);
  }
}
あいう
えお

という結果になります。
String型変数は参照型変数ではないのでしょうか?

最後に

長文の質問となり失礼いたしました。
どなたか有識な方にご回答いただけると幸いです。
また質問・表記方法や理解などに間違いがあれば、そちらだけでもご指摘いただけると嬉しいです。
よろしくお願いいたします!!!

  1. 中山清喬, 国本大悟. 『スッキリわかるJava入門第2版』

1

6Answer

参照型変数にはオブジェクトへの参照値が代入されます。
簡単な図にしてみました。
image.png

2Like

Comments

  1. @tabetaaaaaaa

    Questioner

    わかりやすい図を作っていただいてありがとうございます!
    配列とStringとの動きの違いがめちゃくちゃわかりやすいです・・・

    ちなみにもし可能なら教えていただきたいのですが、この図は何というツールを用いて作成されたのでしょうか・・・?
  2. 図はPowerPointで書いています。
    Ctrl+A で描いた図を全選択して Ctrl+C で図形コピー、そのまま Qiita の投稿欄に Ctrl+V で図を貼り付けられるので便利です。
  3. @tabetaaaaaaa

    Questioner

    パワポをそのまま貼り付けられるんですね!

    重ね重ねありがとうございますm(_ _)m

質問3ついて、

ところで、nullはString型の変数に代入することができると考えています。

はい、できますね

ということは、String型の変数は参照型変数のはずです。

そうですね。

//aとb両方のメモリ区画の内容を書き換えたはず?

ここの理解が違っていて、言葉で説明するのは難しいので、メモリの挙動についてこのページを参考にするとよいと思います(ちょうど似たようなコードを取り扱っています)

1Like

Comments

  1. @tabetaaaaaaa

    Questioner

    ありがとうございます!
    貼っていただいたページを読んでみました。

    - Stringは参照型だけどそれは内部にchar型の配列が入っていてるからであること。
    - そのchar型の配列がprivate finalで定義されているので、書き換えが不可能=イミュータブルであること

    がわかりました!
    わかりやすい記事を教えていただきありがとうございました。

質問1
しかし、配列の「参照を切る」というこの動作は、実際にどのような場面で利用されるのか全くイメージが湧きません。
GCがあるにも関わらず、わざわざ変数の初期化の必要がある場面はどのような時起こるのでしょうか?

仰る通り,使われなくなったオブジェクトは自動的にGCが解放しますが,その際ランタイムに「これもういらないよ」と明示することはできます.
手元で検証していないのですが,GCが具体的にどのような状況で動くかor動かないかはこの辺を見てみると良いと思います.

質問2
では、配列bの参照が切られた後に、
配列bにはメモリ区画が一切割り当てられていない、という認識で正しいでしょうか?
その場合この後配列bを(数値を代入するなどして)利用することは全くできないのでしょうか?

以下のコードとその動作を理解する前に,変数bそのものとbに入っている(配列)インスタンスを区別する必要があります.Cのポインタについての知識がおありなら理解しやすいと思います.
変数自体は,Cでいうところのポインタにすぎません.Javaにおいても,変数の複製をすれば,基本的に同じインスタンスを参照します(プリミティブ型に対しては,これは当てはまりません!)

        int a[] = {1,2,3};
        int b[] = a;
         
        b[2] = 4;
        System.out.println(a[2]);  // -> 4

一方でb=nullするとbはどこも指さなくなります(ので当然,これ以降bに対する操作を行うことはできません).注意点としては,もともとあったインスタンスがただちになくなるわけではありません. bにnullを代入する行為は,bからインスタンスを参照できなくするだけに過ぎません1.

        int a[] = {1,2,3};
        int b[] = a;
         
        b[2] = 4;
        b= null;
        System.out.println(a[2]); // -> 4
        //System.out.println(b[2]); // -> NullPointerException 

そして,どこからも参照されなくなったインスタンスに対して,JavaはGCを動作させるわけです.

質問3
String型変数は参照型変数ではないのでしょうか?

たしかにStringはオブジェクトであるためnullを代入できますが,JavaにおけるString型変数は内容を値渡し(いわゆるコピー)する特性があります.intと同様の動作です2.

元のStringを書き換える必要がある場合は,こんな感じに実装することが考えられますが,オブジェクト指向型言語である以上抽象化をしっかりしないと,後で困ることになるので念のため.

  1. 「割り当て」という単語は,一般的には「実際のデータを格納するためのメモリを確保する」という意味合いで使われることが多いようです.なので「配列bにはメモリ区画が一切割り当てられていない」という文は意味が不正確になる気がします.上でも述べたように変数自体はいわゆるポインタであるため,メモリ区画を「割り当て」ることとは直接関係がありません.ただし,割り当てた場所を知る必要はあるため,変数がその受け皿になるというわけです.

  2. ちなみにプリミティブintのラップであるIntegerなども同じ動作をします.

1Like

Comments

  1. @tabetaaaaaaa

    Questioner

    3つともに答えていただきありがとうございます!

    GCについてわかりやすい記事を貼っていただきありがとうございます。
    恥ずかしながらCに付いての知識はないのですが、
    - 一度nullを代入した変数はもちろん使えなくなるということ
    - ただしそれは、インスタンスを変数から参照できなくなっただけでありインスタンスがなくなったわけではないということ
    と、null代入後の変数の仕組みがよくわかりました!

    また、「割り当て」という日本語の正確な意味を理解していなかったので、とても勉強になりました。

    たくさん答えていただいて本当にありがとうございます!

質問1 わざわざ変数の初期化の必要がある場面はどのような時起こるのでしょうか?

例えばですが、一旦 int [] a = {1, 2, 3}; int [] b = a; というようにしたが、その後何らかの理由で b としては {1, 2, 3} が使えなくなったら、その時点で b = null; としておけば、それ以降は変数 b が null か否かをチェックして誤用を避けることができるということでしょう。

b = null; は「変数の初期化」ではありません。単に変数 b への null の代入です。

質問2 では、配列bの参照が切られた後に、配列bにはメモリ区画が一切割り当てられていない、という認識で正しいでしょうか? その場合この後配列bを(数値を代入するなどして)利用することは全くできないのでしょうか?

そもそも b は配列ではありません。int 型の配列のインスタンスのアドレスを指す変数です。

int [] b = a; の時点で、変数 b 用のメモリを確保し、先に int [] a = {1, 2, 3}; で生成したint 型の配列 {1, 2, 3} のインスタンスのアドレスを b に代入しています。

b = null; で変数 b のメモリが無くなるわけではないです。変数 b に null が代入されるだけです。つまり、変数 b が指すものは無くなったということです。

変数 b のスコープの範囲内なら、再び b = a; とすることも、新たに別の配列のインスタンスを作ってそのアドレスを代入することもできます。

質問3 しかし以下のようなコードを動かすと・・・b = "えお"; //aとb両方のメモリ区画の内容を書き換えたはず?

「aとb両方のメモリ区画の内容を書き換え」という理解が間違ってます。そうではなくて、その行が実行された時点で a は "あいう" というインスタンスの、b は "えお" というインスタンスを指しています。なので println の結果は当然 a が "あいう" b が "えお" となります。

1Like

Comments

  1. @tabetaaaaaaa

    Questioner

    nullを代入する際の例を教えていただきありがとうございます!
    調べてもなかなか出てこなかったので嬉しいです。

    「変数にnullを代入すること」は変数が指すものがなくなったということで、「変数を初期化する」ということとイコールではないということも理解できました!
    nullを代入した変数に新しい値を代入することはできなくても、アドレスを代入することはできるんですね…

    たくさん例示していただいてありがとうございましたm(_ _)m

ひとまず、例示されたコードを実行したときにどういう結果が得られるのかを確認されたほうが良いですね。

0Like

Comments

  1. @tabetaaaaaaa

    Questioner

    ありがとうございます。
    再確認したところ1つ目のコードに間違った認識を持っていたので、コードと質問内容を一部変更してみました!

    もし教えていただける箇所が改めてあれば、ご教示いただけると嬉しいです。

その場合この後配列bを(数値を代入するなどして)利用することは全くできないのでしょうか?

変数bint[]型なので、int[]型の値を代入(参照)できます。

public class Main {
    public static void main(String[] args) {
        int [] a = {1, 2, 3};
        int [] b = a;
        b = null;  //参照を切った。

        System.out.println("~配列a~");
        for(int x:a){
          System.out.println(x);
        }

        b = new int[] {4, 5};
        System.out.println("~配列b~");
        for(int x:b){
          System.out.println(x);
        }
    }
}
0Like

Your answer might help someone💌