Java
初心者
初心者向け

参照を知らずに開発を続けていいのか

はじめに

後輩に限らず先輩のソースを見たり話していると、どうやら『参照』をわかっていない状態で何となく開発を続けているようです。怖い…:scream: 確かに最初のうちはわかりにくいことだけど、2年も3年も上の先輩が知らないのは正直心が痛む…。というわけで少しだけ参照について書いていこうと思います。ちなみにJavaベースのお話です。

プリミティブ型と参照型

正直この手の話はいろんな記事があると思うので、そちらを参考にしていただきたいです。

Javaのプリミティブ型と参照型について
【Java】 基本データ型 と 参照型 の違い

結論:頭文字が小文字 ⇒ プリミティブ型、大文字 ⇒ 参照型

…というわけにはいかないので少し補足していきます。

= (イコール)は何をしているのか

プリミティブ型について次のような例を考えてみます。

int a = 1;
int b = a;
b = 2;

この場合 a ⇒ 1, b ⇒ 2 なのはわかりますよね?

では次のようなクラスを作り、参照型で似たようなことをやりましょう。(※コーディング的な話は無視してます)

public class MyInteger {
  public int value;
  public MyInteger (int value) { this.value = value; }
}
MyInteger a = new MyInteger(1);
MyInteger b = a;
b.value = 2;

この場合 a.value ⇒ 2, b.value ⇒ 2 になります。

イメージ的にはこんな感じでしょうか…。

1行目から見ていくと、イメージのようにプリミティブ型の a は値そのもの(1)を保存します。
一方参照型は、new MyInteger(1)で生成したオブジェクトの場所(という名の値)を保存します。つまりa.valueは何を表しているかというと、「aの場所にあるオブジェクトのvalueの値」ということになります。

2行目は実はどちらも同じようにaの値をbにコピーしています。プリミティブ型の場合は1を、参照型の場合はオブジェクトの場所をコピーしていることになります。(俗にいう『値渡し』)。

3行目のプリミティブ型は説明不要ですよね。参照型は1行目で説明したように「bの場所にあるオブジェクトのvalueの値 = 2」といったことをしています。2行目でbはaの場所をコピーしたことを考えれば、a.value ⇒ 2, b.value ⇒ 2となるわけです。

簡単にまとめ

  • プリミティブ型は値そのものを、参照型はオブジェクトの参照値を保存する。
  • b = aとは、型に限らずa の(参照)値をbにコピーする(値渡し)ことである。

少しだけ余談

値渡しと聞くと合わせて『参照渡し』という言葉も耳にします。上記の参照型の例をよく「参照渡しだ!」という方がいますが、Javaには参照渡しはありません。このことについては、以下の記事を参考にしていただければと思います。

もう参照渡しとは言わせない

昔は確かに参照渡しだと思ってたなぁ…。

== は何を意味するのか

文字列(String)を比較する場合に、うっかりa == bのように比較してしまうことはJava初心者にはありがちです。正しくはa.equals(b)ですよね?

そもそも「==」は何を意味しているでしょうか。

//プリミティブ型
int a = 1;
int b = 1;
System.out.println(a == b);  //⇒ true
//参照型
MyInteger a = new MyInteger(1);
MyInteger b = new MyInteger(1);
System.out.println(a == b); //⇒ false

プリミティブ型 a == bは aとbの値は同じであるということは言うまでもありませんよね。

参照型はどうでしょうか。1という値は同じなので一見a == b ⇒ trueと最初は思いがちです。しかし、参照型であろうと「==」は変わらず、aとbの(参照)値は同じあるということを意味します。a,bは別のオブジェクトの参照値を保存しているのでa == b ⇒ falseとなるわけです。

equals()メソッドとは

ではa.equals(b)は何を意味しているのでしょうか。結論から言うと、実装によりけりです。

そもそもequals()はすべてのクラスのスーパークラスである「Objectクラス」で次のように実装されています。

public boolean equals(Object obj) {
  return (this == obj);
}

つまり以下のようにMyIntegerオブジェクトを比較しても「==」と同じ結果になります。

MyInteger a = new MyInteger(1);
MyInteger b = new MyInteger(1);
System.out.println(a.equals(b)); //⇒ false

例えばMyIntegerクラスのequals()に「aとbのvalueの値が同じである」という意味を持たせたい場合、以下のようにをオーバーライドする必要があります。

@Override
public boolean equals(Object obj) {
  //同じオブジェクトを参照している場合は、当然値も一緒である
  if (this == obj) {
    return true;
  }
  //別クラスのオブジェクトは比較するまでもなく別物
  if (!obj instanceof MyInteger) {
    return false;
  }
  MyInteger other = (MyInteger) obj; 
  return (this.value == other.value);
}

これを実装することで先ほどのa.equals(b)はtrueとなるわけです。

このようにequals()を実装するのは自分自身のため、好きなように意味を持たせればいいんです。
ただし、すべてのフィールドの値が同じであるというのが基本なのはお忘れなく。

またまた余談

次のようにStringを比較してみます。

String a = "String!!";
String b = "String!!";

System.out.println(a == b);       //⇒ true
System.out.println(a.equals(b));  //⇒ true

あれ…。equals()がtrueになるのはわかるけど、== がtrueになっている??
実は2行目の"String!!"では新しくオブジェクトは生成はされず、1行目の"String!!"を使いまわしているようです。

【Java入門】文字列(String)を比較する方法(「==」と「equals」)

オブジェクトをコピーするには

参照型a = bはオブジェクトのコピーではなく参照値のコピーということは先述の通りです。ではオブジェクトはどのようにすればコピーできるのか、MyIntegerクラスと次のCloneTestクラスで説明していきます。

class CloneTest {
  public int num;
  public MyInteger myInt;
  public CloneTest (int num) {
    this.num = num;
    this.myInt = new MyInteger(num); 
  }

  @Override
  public boolean equals(Object obj) {
    //ちょいと省略
    CloneTest other = (CloneTest) obj;
    return this.num == other.num && this.myInt.equals(other.myInt);
  }
}

clone()メソッド

Objectクラスにはオブジェクトをコピーするためのclone()というメソッドが用意されています。大まかにclone()の説明をすると、フィールドの値をすべてコピーした別オブジェクトを作成することです。これを用いてclone()メソッドを実装すればよいのですが、以下の注意点があります。

  • Cloneableインターフェースを実装(implements)する
    • 実装をしていない場合は例外:CloneNotSupportedException が投げられる
  • 参照型をフィールドに持つ場合、明示的にコピー(clone)をする
    • Objectクラスのclone()はフィールドの参照値をコピーするだけなので

では実際にMyIntegerクラスとCloneTestクラスにclone()を実装して比較してみます。

class MyInteger implements Cloneable {
  //省略

  @Override
  public MyInteger clone() {
    try {
      return (MyInteger) super.clone();
    } catch (CloneNoSupportedException e) {
      throw new AssertionError();
    }
  }
}
class CloneTest implements Cloneable {
  //省略

  @Override
  public CloneTest clone() {
    try {
      CloneTest result = (CloneTest) super.clone();
      result.myInt = this.myInt.clone();
      return result;
    } catch (CloneNotSupportedException e) {
      throw new AssertionError();
    }
  }
}
CloneTest a = new CloneTest(1);
CloneTest b = a.clone();

System.out.println(a == b);       //⇒ false
System.out.println(a.equals(b));  //⇒ true

比較結果からオブジェクトがコピーできていることがわかります。

コピーコンストラクタ

引数に自クラスオブジェクトを渡し、そのオブジェクトと同じフィールドを持つオブジェクトを生成するコンストラクタをコピーコンストラクタといいます。

先ほど同じように実際に実装して比較してみます。

public MyInteger(MyInteger org) {
  this.value = org.value;
}
public CloneTest(CloneTest org) {
  this.num = org.num;
  this.myInt = new MyInteger(org.myInt);
}
CloneTest a = new CloneTest(1);
CloneTest b = new CloneTest(a);

System.out.println(a == b);       //⇒ false
System.out.println(a.equals(b));  //⇒ true

比較結果からclone()と同じくオブジェクトのコピーができていることがわかります。

※ clone()とコピーコンストラクタのどちらを使えばいいのかという話がありますが、今回は取り上げません。

おわりに

参照の基礎的なことについて紹介しました。せめて先輩にはわかっていてほしいものですね…。
余談:図作成にいいツールはないものか…。