Open6

Javaと言うとかなり語弊がありますが、Javaの【一部の】文脈で「シャローコピー」と「ディープコピー」が指す内容(アルゴリズム)と、JavaScriptとは異なっているかも知れません

ピン留めされたアイテム
suinsuin

背景

JavaとJavaScriptでシャローコピーが指す内容がずれているのか?という質問をGitHubでもらった。

JavaとJavaScriptでのシャローコピーとディープコピーの違いについて質問です · Issue #605 · yytypescript/book

質問で想定された対応関係

言語 対応関係1 対応関係2 対応関係3
Java シャローコピー ディープコピー ディープコピー
JavaScript ??? シャローコピー ディープコピー

個人的には言語によってこんなずれは無いと思いつつ、Javaにそこまで詳しくないので調査してみることにした。

結論

  • (少なくとも英語圏と一部の日本語圏では) JavaとJavaScriptで「シャローコピー」と「ディープコピー」は同じようなものを指し、対応関係にある
  • ただし、日本語圏の【一部】ではJavaの参照渡しが「シャローコピー」とする解説、または、誤解を招く可能性がある解説が存在していて、Javaを使っている人の中にもJavaとJavaScriptで「シャローコピー」が異なるものを指し、対応関係にないと理解する人がいて、いびつな構造になっている

注意: Javaの一部というと語弊があるかもしれません

https://twitter.com/cactaceae/status/1630669581856698368

  • Javaのほうが参照渡し=シャローコピーという情報に触れる機会が多そうというのが実態かもしれない。

文脈

このスクラップは「参照渡しがシャローコピーとする解説、または、そういう理解する可能性がある解説を直してくれー!」という意図や目的はなく、仮にそういう理解を持った人がTypeScriptを学ぶとき、混乱しないような解説を僕たちが書いている『サバイバルTypeScript』という入門書で行いたいというのがこの調査の目的となります。

suinsuin

MDNによるJavaScriptのシャローコピーの説明

オブジェクトのシャローコピーとは、コピーがコピー元のオブジェクトとプロパティにおいて同じ参照を共有する(同じ基礎値を指す)コピーのことを指します。その結果、コピー元とコピー先のどちらかを変更すると、もう一方のオブジェクトも変更される可能性があります。そのため、意図せずにコピー元やコピー先に予期しない変更が発生してしまう可能性があります。この挙動は、ソースとコピーが完全に独立しているディープコピーの挙動とは対照的です。

JavaScript では、すべての標準組込みオブジェクトのコピー操作(スプレッド構文, Array.prototype.concat(), Array.prototype.slice(), Array.from(), Object.assign(), Object.create())において、ディープコピーではなくシャローコピーを生成します。

https://developer.mozilla.org/ja/docs/Glossary/Shallow_copy

suinsuin

Javaのシャローコピー

JavaScriptの「別の変数への代入=(オブジェクトの参照の共有)」がJavaの「シャローコピー」?

package main;

import entity.User;

public class Start {

  public static void main(String[] args) {

    User user1 = new User();
    // user1の名前を設定
    user1.setName("変更前の名前");

    // シャローコピー
    User user2 = user1;
    // user2の名前を変更
    user2.setName("変更後の名前");

    System.out.println("user1の名前:" + user1.getName());
    System.out.println("user2の名前:" + user2.getName());
  }

}

https://medium-company.com/ディープコピーとシャローコピーの違い/


public class Test {
	public static void main(String[] args) throws CloneNotSupportedException {

		//インスタンスを生成
		CloneableObject origin = new CloneableObject();
		origin.value = 1;
		origin.source = "おはよう";
		origin.list.add(10);

		//インスタンスのディープコピー
		CloneableObject copy = origin.clone();
		copy.value = 2;
		copy.source = "おやすみ";
		copy.list.add(20);

		//出力メソッド実行
		origin.dump("コピー元");
		copy.dump("コピー先");
	}
}

https://workteria.forward-soft.co.jp/blog/detail/10200

商業本での解説

『Java本格入門』, p.418, 第3刷にて、以下のように説明されています。
”参照型の変数を別の変数に代入するだけの場合、代入によって参照のみがコピーされます。そのため、コピー先の変数のプロパティの値を変更しても、オブジェクトの実体はコピー前のオブジェクトと同一のため、コピー元の変数から見えるオブジェクトの値も書き換わってしまいます。このようなコピーの方法を「シャローコピー」と呼びます。(中略)
コピーして複数の異なるオブジェクトを作成したいのであれば、参照をコピーするのではなく、オブジェクト自体をコピーする必要があります。この方法でコピーすると、コピー先の変数のプロパティの値を変更しても、コピー元の変数から参照されるオブジェクトの値は書き換わりません(実体は別オブジェクトのため)。これを「ディープコピー」と呼びます。”

これについてTwitterで、「こういうのもJavaではシャローコピーと言うのか?」と聞いてみたところ、シャローコピーとは言えないだろうという反応が返ってきた。

https://twitter.com/kagilinn/status/1630149559388377089?conversation=none

UPDATE 2023-03-01 7:21

『Java本格入門』著者の方によれば、シャローコピーを参照渡しとして説明する意図はないとのことです。

https://twitter.com/cero_t/status/1630687276245336065

商業本にも誤ったシャローコピーの説明があるかのような記載をしてしまい申し訳ございません。このことをふまえると、確認された事実としては、筆者の意図に反して、読者が誤読された可能性があるというところまでです。

suinsuin

Javaの英語圏の資料ではshallow copyはJavaScriptのシャローコピーと同じ概念として説明している

https://stackoverflow.com/questions/1175620/in-java-what-is-a-shallow-copy

class Foo {
  private Bar myBar;
  ...
  public Foo shallowCopy() {
    Foo newFoo = new Foo();
    newFoo.myBar = myBar;
    return newFoo;
  }

  public Foo deepCopy() {
    Foo newFoo = new Foo();
    newFoo.myBar = myBar.clone(); //or new Bar(myBar) or myBar.deepCopy or ...
    return newFoo;
  }
}

Foo myFoo = new Foo();  
Foo sFoo = myFoo.shallowCopy();  
Foo dFoo = myFoo.deepCopy();  

myFoo.myBar == sFoo.myBar => true  
myFoo.myBar.equals(sFoo.myBar) => true  
myFoo.myBar == dFoo.myBar => **false**  
myFoo.myBar.equals(dFoo.myBar) => true  
suinsuin

Oracle的には、JavaのシャローコピーとJavaScriptのシャローコピーと同じ考え方

cloneメソッドの説明の中に次のような文章が出てくる

実装していない場合、このメソッドはこのオブジェクトのクラスの新しいインスタンスを生成し、そのフィールドをすべて、このオブジェクトの対応する各フィールドの内容で初期化します。これは代入と同様で、フィールドの内容自身が複製されるのではありません。 つまりこのメソッドは、オブジェクトの「シャロー・コピー」を生成しますが、「ディープ・コピー」は生成しません。

https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/lang/Object.html#clone()

suinsuin

Javaでは日本語圏の一部と英語圏で「シャローコピー」に対する理解が異なっていそう

日本語圏のJavaの解説の一部は、参照渡しのことをシャローコピーと説明するものがあるため、理解が異なっている人がいそう。

言語 対応関係1 対応関係2 対応関係3
Java (日本語圏の一部) シャローコピー ディープコピー 何と言う?
Java (英語圏と日本語圏の一部) 参照渡し シャローコピー ディープコピー
JavaScript 参照渡し[1] シャローコピー ディープコピー

世間の認識

Twitterでアンケートをとったところ、参照渡しをシャローコピーだとする人が一定数いた。

https://twitter.com/suin/status/1630053860013391872

  • User u2 = u1; 21.9%
  • User u2 = u1.clone(); 24%
  • どちらでもない 12%

cloneが常にシャローコピーであるとは限らないので、このアンケートの仕方が良くない部分もあるが、それでもu2 = u1に投じる人が一定数いることは特筆すべき事実である。

「普通のJavaクラスタ」なら u2 = u1は選ばないという意見も確認された。

https://twitter.com/yoshi389111/status/1630071725697433600

ChatGPTも日本語で聞くのと英語で聞くのでは異なる回答

ChatGPTにJavaのシャローコピーのサンプルコードを書かせてみた。質問は日英の2言語で作り、それぞれ別セッションで行った。

日本語で質問したら、参照渡しのコードが提供された

Person:

  • Javaのオブジェクトのシャローコピーのサンプルコードを書いて下さい。

ChatGPT:

  • Javaでオブジェクトのシャローコピーを作成するには、オブジェクトを新しい変数に代入するだけです。以下は、シャローコピーのサンプルコードです。
public class ShallowCopyExample {
    public static void main(String[] args) {
        // オリジナルのPersonオブジェクト
        Person originalPerson = new Person("John", 30);

        // シャローコピーの作成
        Person shallowCopyPerson = originalPerson;

        // シャローコピーを変更する
        shallowCopyPerson.setAge(40);

        // オリジナルのPersonオブジェクトを出力する
        System.out.println("オリジナルのPersonオブジェクト: " + originalPerson);

        // シャローコピーを出力する
        System.out.println("シャローコピーのPersonオブジェクト: " + shallowCopyPerson);
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

この例では、オリジナルのPersonオブジェクトを作成し、そのオブジェクトをシャローコピーとして別の変数に代入しています。そして、シャローコピーの年齢を変更しています。この変更は、オリジナルのPersonオブジェクトにも反映されることがわかります。

実行結果は以下のようになります。

オリジナルのPersonオブジェクト: Person{name='John', age=40}
シャローコピーのPersonオブジェクト: Person{name='John', age=40}

英語で質問したら、cloneを用いたコードが提供された

Person:

  • Cloud you provide a sample code for shallow copy of Java object?

ChatGPT:

  • Sure, here's an example code for shallow copy of a Java object:
public class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;

    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        Address address = new Address("123 Main St", "Anytown", "USA");
        Person originalPerson = new Person("John Doe", 30, address);

        // Perform a shallow copy
        Person clonedPerson = originalPerson.clone();

        // Modify the address in the cloned object
        clonedPerson.getAddress().setCity("Newtown");

        // Print the address in both objects
        System.out.println("Original person address: " + originalPerson.getAddress());
        System.out.println("Cloned person address: " + clonedPerson.getAddress());
    }
}

class Address {
    private String street;
    private String city;
    private String country;

    public Address(String street, String city, String country) {
        this.street = street;
        this.city = city;
        this.country = country;
    }

    public String getStreet() {
        return street;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getCountry() {
        return country;
    }

    @Override
    public String toString() {
        return street + ", " + city + ", " + country;
    }
}

In this example, we have a Person class that has a name, age, and an Address object. The Address class has a street, city, and country. We implement the Cloneable interface and override the clone() method to create a shallow copy of the Person object. In the main method, we create an instance of the Person class with an Address object, and then create a shallow copy of this object.

We then modify the city in the address of the cloned object and print the address in both the original and cloned objects. Since we're performing a shallow copy, modifying the address in the cloned object also modifies the address in the original object. Therefore, when we print the address in both objects, we see that they both contain the modified city "Newtown".

脚注
  1. 参照渡しというより、参照の共有と言ったほうが正しいかもしれない ↩︎