Javaで配列をコピーする。シャーロンコピーとディープコピーの違いとは?

Java

Javaで配列をコピーする際、気を付けなければいけないポイントがあります。
シャーロンコピーとディープコピーの違いです。


この2つの挙動の違いを把握しておかないと、思わぬバグを埋め込んでしまうことなります。


今回は、シャーロンコピーとディープコピーの違いを、サンプルプラグラムをもとに説明していきます。


環境情報


  • OS:Windows10
  • Java:1.8.0_60

シャーロンコピーとは?ディープコピーとは?


シャーロンコピーとディープコピーとは何でしょうか?

まずは、この2つのコピーの違いについて説明します。


シャーロンコピー


C言語を経験した人の場合は、アドレスコピーときくとピンとくるかと思います。
要素を指定するアドレスをポインタを使用してコピーしており、要素がもつ値はコピーしていないです。


ディープコピー


値コピー、と言い換えることができます。
シャーロンコピーとは異なり、アドレスではなく、要素そのものをコピーします。


シャーロンコピーの概念が理解し辛い人が多い気がします。
C言語を使った人はわかるかと思いますが、「ポインタ」のイメージですね。


シャーロンコピーとディープコピーのサンプルプログラム


それでは、さっそくですがサンプルプログラムです。

例として、以下のサンプルプログラムになります。

  • コピー対象は、独自エントリクラス(Entry)の配列を「ArrayList」で保持する。
  • シャーロンコピー→ディープコピーの順番でコピーを実施。
  • コピー元配列の内容を表示。

サンプルプログラム


作成するクラスは2つになります。
「CopyTest.java」がコピーするメインクラス、「Entry.java」が配列要素となるエントリクラスです。


CopyTest.java

import java.util.ArrayList;

public class CopyTest {

    // メイン処理
    public static void main(String[] args){
        System.out.print("start: CopyTest\r\n");

        /**
        ①:エントリ内容の作成と確認
        */
        ArrayList<Entry> srcList = new ArrayList<Entry>();
        Entry newEnt1 = new Entry();
        newEnt1.setSei("Yamada");
        newEnt1.setNa("Taro");
        srcList.add(newEnt1);
        Entry newEnt2 = new Entry();
        newEnt2.setSei("Suzuki");
        newEnt2.setNa("jiro");
        srcList.add(newEnt2);
        System.out.print("エントリ内容の作成確認\r\n");
        dispList(srcList);

        /**
        ②:シャーロンコピー
        */
        ArrayList<Entry> shtList = srcList;
        Entry entry = shtList.get(0);
        entry.setSei("Satou");
        entry.setNa("Saburo");
        System.out.print("リストをコピー確認\r\n");
        dispList(srcList);

        /**
        ③:ディープコピー
        */
        ArrayList<Entry> deList = 
            new ArrayList<Entry>(srcList.size());
        for (Entry ent : srcList) {
            deList.add(ent.clone());
        }
        entry = deList.get(0);
        entry.setSei("Tanaka");
        entry.setNa("Shiro");
        System.out.print("リストをコピー確認\r\n");
        dispList(srcList);

        System.out.print("end: CopyTest\r\n");
    }

    private static void dispList(ArrayList<Entry> dispList){
        for (Entry dispEnt : dispList) {
            System.out.print("sei = " + dispEnt.getSei() + "\r\n");
            System.out.print("na = " + dispEnt.getNa() + "\r\n");
        }
    }
}

Entry.java

public class Entry implements Cloneable {
    private String sei;
    private String na;

    // 姓の設定
    public void setSei(String sei) {
        this.sei = sei;
    }

    // 名の設定
    public void setNa(String na) {
        this.na = na;
    }

    // 姓の取得
    public String getSei() {
        return this.sei;
    }

    // 名の取得
    public String getNa() {
        return this.na;
    }

    // クーロン
    public Entry clone() {
        try {
            return (Entry) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

シャーロンコピーの実行結果


サンプルプログラムの②の部分がシャーロンコピーのサンプルプログラムです。
シャーロンコピーを実行した結果は以下になります。

sei = Satou
na = Saburo
sei = Suzuki
na = jiro

実際にシャーロンコピーしている部分は以下です。

ArrayList<Entry> shtList = srcList;

シャーロンコピーしたコピー先リスト(shtList)の内容を変更しています。
エントリの1つ目について、以下のように変更しています。

・Sei=Satou 
・Na=Saburo

シャーロンコピーしたコピー先リスト(shtList)を変更したにも関わらず、コピー元のリスト(srcList)が変更されています。
これはなぜかというと、コピーしたのはあくまでアドレスであるシャーロンコピーをおこなっているからになります。
変更先リストを変更しているにも関わらず変更元リストが変更されている、という事です。


ディープコピーの実行結果


サンプルプログラムの③の部分がディープコピーのサンプルプログラムです。
ディープコピーを実行した結果は以下になります。

sei = Satou
na = Saburo
sei = Suzuki
na = jiro
end: CopyTest

実際のディープコピーは、「Entry」クラスのcloneメソッドになります。

ArrayList<Entry> deList = new ArrayList<Entry>(srcList.size());
for (Entry ent : srcList) {
    deList.add(ent.clone());
}

要素を完全にコピーしています。
完全にコピーした要素に対して変更しているので、変更元の配列要素は変更されていないです。


これがディープコピーになります。
通常は、上記のようにcloneメソッドを準備して、要素を完全コピーする実装を準備しておきます。


よく、様々なウェブサイトで以下のように実装すればディープコピーされると記載されているのですが、それは間違いです。
気を付けてください。

ArrayList<Entry> deList = new ArrayList<Entry>(srcList);

まとめ


ディープコピーとシャーロンコピーの違いを正しく理解してプログラミングしましょう!


よくあるのが、ディープコピーしていると思っていたがシャーロンコピーで、コピー元の要素が変更されてしまった!というパターンです。
思わぬバグを作りこんでしまう事になります。



cpコマンド(コピー)の使い方。覚えておくと便利なオプションも解説。

Linux

前回、lsコマンドの使い方、覚えておくと便利なオプションについて紹介させてもらいました。



今回は、コピーコマンドについて紹介していきます。
コピーコマンドについても、Linuxで作業をおこなう上では必須のコマンドになります。
こちらもまた、使わない人はいないでしょう。


メジャーなオプションからマイナーなオプションまで、いろいろ説明していきます。


頻繁に使うコマンドなのであまり忘れることはないオプションが多いかもしれませんが、参考にしてください。


同名ファイルが存在する場合はバックアップ作成(-b)


コピー先に同名ファイルが存在する場合、バックアップファイルを作成します。
「-b」オプションを使用します。


$ ls -la
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:31 .
drwxr-xr-x 5 xxx yyy 4096 2019-07-28 23:49 ..
-rw-r--r-- 1 xxx yyy    5 2020-01-29 01:31 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:31 src.txt
$ cp -b src.txt dest.txt
$ ls -la
total 20
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:31 .
drwxr-xr-x 5 xxx yyy 4096 2019-07-28 23:49 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:31 dest.txt
-rw-r--r-- 1 xxx yyy    5 2020-01-29 01:31 dest.txt~
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:31 src.txt

上記コマンド結果のようになります。
コピー先に既に「dest.txt」というファイルが既に存在するので、コピー先に存在していた「dest.txt」をバックアップファイルとして退避しています。


強制上書き(-f)


OS自体の設定にもよりますが、ファイルをコピーすると上書きになる場合、「上書きします。よろしいですか?」のメッセージが表示されます。
「-f」オプション、または、「–force」オプションを使用すると、この上書き確認メッセージを表示しません。


$ ls -la
total 12
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:34 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    0 2020-01-29 01:36 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:34 src.txt
$ cp -f src.txt dest.txt
$ ls -la
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:34 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:34 src.txt

上記コマンド結果のように、同じファイル名でコピーしたとしても、上書き確認メッセージは表示されません。


必ず上書き確認メッセージを表示(-i)


先ほどの強制上書きでも記載しましたが、「上書きします。よろしいですか?」のメッセージが表示されるかどうかは、OS自体の設定に依存します。
「-i」オプション、または、「–interactive」オプションを使用すると、上書き確認メッセージを必ず表示することができます。


$ ls -la
total 12
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:49 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    0 2020-01-29 01:49 src.txt
$ cp -i dest.txt src.txt
cp: overwrite `src.txt'? yes
$ ls -la
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 01:49 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:49 src.txt

上記コマンド結果のように、上書きコピー時に「 overwrite `src.txt’?」という確認メッセージを表示しています。
yesと入力すると、ファイルが上書き更新されます。


同名ファイルの場合は上書きしない(-n)


同名ファイルを上書きしたくない場合は、「-n」オプション、または、「–no-clobber」オプションを指定すると、上書きは行われません。


$ ls -la
total 12
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 02:08 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    0 2020-01-29 02:04 src.txt
$ cp -n dest.txt src.txt
$ ls -la
total 12
drwxr-xr-x 2 xxx yyy 4096 2020-01-29 02:08 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    0 2020-01-29 02:04 src.txt

属性情報(タイムスタンプなど)をそのままにしてコピー(-p)


タイムスタンプ・グループ・所有者といった属性情報はそのままにしてコピーする場合は「-p」オプション、または、「–preserve」オプションを指定します。


$ ls -la
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:35 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-02-05 00:38 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 src.txt
$ cp -p src.txt dest.txt
$ ls -la
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:35 .
drwxr-xr-x 5 xxx yyy 4096 2020-01-29 01:36 ..
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 src.txt

フォルダをコピー/再帰的にコピー(-r)


フォルダをコピーする場合は「-r」オプションを指定します。

このオプションを指定すれば、フォルダの中身がそのまま表示されます。

フォルダをコピーというよりも、フォルダの中身を再帰的にコピーするという動きになります。


$ ls -la
total 12
drwxr-xr-x 3 xxx yyy 4096 2020-02-05 00:49 .
drwxr-xr-x 8 xxx yyy 4096 2019-09-10 01:01 ..
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:35 dir
$
$
$ ls -la ./dir/
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:35 .
drwxr-xr-x 3 xxx yyy 4096 2020-02-05 00:49 ..
-rw-r--r-- 1 xxx yyy    4 2020-02-05 00:40 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-01-29 01:43 src.txt
$ cp -r dir dir2
$ ls -la
total 16
drwxr-xr-x 4 xxx yyy 4096 2020-02-05 00:50 .
drwxr-xr-x 8 xxx yyy 4096 2019-09-10 01:01 ..
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:35 dir
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:50 dir2
$ ls -la ./dir2/
total 16
drwxr-xr-x 2 xxx yyy 4096 2020-02-05 00:50 .
drwxr-xr-x 4 xxx yyy 4096 2020-02-05 00:50 ..
-rw-r--r-- 1 xxx yyy    4 2020-02-05 00:50 dest.txt
-rw-r--r-- 1 xxx yyy    4 2020-02-05 00:50 src.txt