Дублирование объектов в Java

Я узнал, что когда вы изменяете переменную в Java, она не меняет переменную, на которой она основана на

int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected

Я принял аналогичную вещь для объектов. Рассмотрим этот класс.

public class SomeObject {
    public String text;

    public SomeObject(String text) {
        this.setText(text);
    }

    public String getText() {
        return text;
    }   

    public void setText(String text) {
        this.text = text;
    }
}

После того, как я попробовал этот код, я смутился.

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected

Пожалуйста, объясните мне, почему изменение любого из объектов влияет на другое. Я понимаю, что значение переменной текста сохраняется в одном и том же месте в памяти для обоих объектов.

Почему значения для переменных независимы, но коррелируются для объектов?

Также, как дублировать SomeObject, если простое присваивание не выполняет работу?

Ответ 1

Каждая переменная в Java является ссылкой. Поэтому, когда вы делаете

SomeClass s2 = s1;

вы просто указываете s2 на тот же объект, на который указывает s1. Фактически вы назначаете значение ссылки s1 (которое указывает на экземпляр SomeClass) на s2. Если вы измените s1, будет также изменен s2 (поскольку он указывает на тот же объект).

Существует исключение, примитивные типы: int, double, float, boolean, char, byte, short, long. Они хранятся по значению. Поэтому при использовании = вы присваиваете только значение, но не можете указывать на один и тот же объект (потому что это не ссылки). Это означает, что

int b = a;

устанавливает значение b в значение a. Если вы измените a, b не изменится.

В конце дня все назначается по значению, это просто значение ссылки, а не значение объекта (за исключением примитивных типов, как указано выше).

Итак, в вашем случае, если вы хотите сделать копию s1, вы можете сделать это следующим образом:

SomeClass s1 = new SomeClass("first");
SomeClass s2 = new SomeClass(s1.getText());

В качестве альтернативы вы можете добавить конструктор копирования в SomeClass, который принимает экземпляр в качестве аргумента и копирует его в свой собственный экземпляр.

class SomeClass {
  private String text;
  // all your fields and methods go here

  public SomeClass(SomeClass copyInstance) {
    this.text = new String(copyInstance.text);
  }
}

С этим вы можете легко скопировать объект:

SomeClass s2 = new SomeClass(s1);

Ответ 2

@brimborium ответ очень хороший (+1 для него), но я просто хочу подробнее рассказать об этом, используя некоторые цифры. Сначала возьмем примитивное назначение:

int a = new Integer(5);
int b = a;
b = b + b;
System.out.println(a); // 5 as expected
System.out.println(b); // 10 as expected
int a = new Integer(5);

1. Первый оператор создает объект Integer значения 5. Затем, назначив его переменной a, объект Integer будет распакован и сохранен в a как примитив.

После создания объекта Integer и перед назначением:

enter image description here

После назначения:

enter image description here

int b = a;

2- Это просто прочитает значение a, а затем сохранит его в b.

(Объект Integer теперь имеет право на сбор мусора, но не обязательно собирать мусор еще в этот момент)

enter image description here

b = b + b;

3- Это дважды читает значение b, добавляет их вместе и помещает новое значение в b.

enter image description here


С другой стороны:

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;
s2.setText("second");
System.out.println(s1.getText()); // second as UNexpected
System.out.println(s2.getText()); // second as expected
SomeObject s1 = new SomeObject("first");

1- Создает новый экземпляр класса SomeObject и присваивает ему ссылку s1.

enter image description here

SomeObject s2 = s1;

2- Это приведет к тому, что ссылка s2 указывает на объект, на который указывает s1.

enter image description here

s2.setText("second");

3- Когда вы используете сеттеры для ссылки, он изменит объект, на который ссылается ссылка.

enter image description here

System.out.println(s1.getText());
System.out.println(s2.getText());

4 Оба должны печатать second, так как две ссылки s1 и s2 относятся к одному и тому же объекту (как показано на предыдущем рисунке).

Ответ 3

Когда вы это сделаете

SomeObject s1 = new SomeObject("first");
SomeObject s2 = s1;

у вас есть 2 ссылки на один и тот же объект. Это означает, что какой бы ссылочный объект вы используете, сделанные вами изменения будут видны при использовании второй ссылки.

Подумайте об этом так: у вас есть один телевизор в комнате, но два пульта дистанционного управления: неважно, какой удаленный вы используете, вы все равно будете вносить изменения в один и тот же базовый объект (телевизор).

Ответ 4

Когда вы пишете:

SomeObject s1 = new SomeObject("first");

s1 не является SomeObject. Это ссылка на объект SomeObject.

Следовательно, если вы назначаете s2 в s1, вы просто назначаете ссылки/дескрипторы, а базовый объект тот же. Это важно в Java. Все передается по значению, но вы никогда не пропускаете объекты вокруг - только ссылки на объекты.

Поэтому, когда вы назначаете s1 = s2, а затем вызываете метод на s2, который изменяет объект, базовый объект изменяется и отображается, когда вы ссылаетесь на объект через s1.

Это один аргумент для неизменности объектов. Делая объекты неизменными, они не будут меняться под вами и, таким образом, будут вести себя более предикативно. Если вы хотите скопировать объект, самым простым/наиболее практичным методом является создание метода copy(), который просто создает новую версию и копирует ее по полям. Вы можете делать умные копии, используя сериализацию/отражение и т.д., Но это более сложное, очевидно.

Ответ 5

От Десять ошибок Программисты Java делают:

6 - Путаница при прохождении по значению и передача по ссылке

Это может быть неприятной проблемой для диагностики, потому что, когда вы смотрите в коде, вы можете быть уверены, что его передача по ссылке, но найти что его фактически передают по значению. Java использует оба, поэтому вам нужно понимать, когда вы проходите по значению, и когда вы проходите мимо ссылка.

Когда вы передаете примитивный тип данных, например char, int, float или double, к функции, то вы проходите по значению. Это означает, что копия типа данных дублируется и передается функции. Если функция решает изменить это значение, она будет изменять копировать только. Как только функция завершается, и управление возвращается на возвращающая функция, "реальная" переменная будет нетронутой, и нет изменения будут сохранены. Если вам нужно изменить примитивные данные тип, сделать это возвращаемым значением для функции или обернуть ее внутри объект.

Поскольку int является примитивным типом, int b = a; является копией по значению, что означает, что a и b - это два разных объекта, но с одинаковым значением.

SomeObject s2 = s1; сделайте s1 и s2 две ссылки одного и того же объекта, поэтому, если вы измените один, другой будет изменен.

Хорошим решением является реализация другого конструктора, подобного этому:

public class SomeObject{

    public SomeObject(SomeObject someObject) {
        setText(someObject.getText());
    }

    // your code

}

Затем используйте его так:

SomeObject s2 = new SomeObject(s1);

Ответ 6

В вашем коде s1 и s2 будут одни и те же объекты (вы только создали один объект с помощью new), и вы можете s2 указать на тот же объект в следующей строке. Поэтому, когда вы меняете text, он меняет оба значения, если вы ссылаетесь на значение через s1 и s2.

Оператор + для целых чисел создает новый объект, он не меняет существующий объект (поэтому добавление 5 + 5 не дает 5 нового значения 10...).

Ответ 7

Это потому, что JVM хранит указатель на s1. Когда вы вызываете s2 = s1, вы в основном говорите, что указатель s2 (т.е. Адрес памяти) имеет то же значение, что и для s1. Поскольку оба они указывают на одно и то же место в памяти, они представляют собой одно и то же.

Оператор = назначает значения указателя. Он не копирует объект.

Клонирование объектов - это сложный вопрос сам по себе. У каждого объекта есть метод clone(), который может возникнуть у вас, но он делает мелкую копию (что в основном означает, что она делает копию объекта верхнего уровня, но любой объект, содержащийся в нем, не клонируется). Если вы хотите поиграть с копиями объектов, обязательно прочитайте Joshua Bloch Эффективная Java.

Ответ 8

Начните со второго примера:

Этот первый оператор присваивает новый объект s1

SomeObject s1 = new SomeObject("first");

Когда вы выполняете присвоение во втором выражении (SomeObject s2 = s1), вы указываете, что s2 указывает на тот же объект, на который в данный момент указывается s1, поэтому у вас есть две ссылки на один и тот же объект.

Обратите внимание, что вы не дублировали SomeObject, а две переменные просто указывали на один и тот же объект. Поэтому, если вы изменяете s1 или s2, вы фактически изменяете один и тот же объект (обратите внимание, если вы сделали что-то вроде s2 = new SomeObject("second"), они теперь будут указывать на разные объекты).

В вашем первом примере a и b являются примитивными значениями, поэтому изменение одного не повлияет на другое.

Под капотом Java все объекты работают с использованием pass by value. Для объектов вы передаете "значение", которое вы передаете, - это местоположение объекта в памяти (поэтому он, похоже, имеет аналогичный эффект прохождения по ссылке). Примитивы ведут себя по-разному и просто передают копию значения.

Ответ 9

Ответы, приведенные выше, объясняют поведение, которое вы видите.

В ответ на "Также, как дублировать SomeObject, если простое назначение не выполняет эту работу?" - попробуйте найти cloneable (это java-интерфейс, который обеспечивает один способ копирования объектов) и copy constructors (альтернативный и, возможно, лучший подход)

Ответ 10

Назначение объекта для ссылки не клонирует ваш объект. Ссылки похожи на указатели. Они указывают на объект, и когда операции вызывается, это делается на объекте, указанном указателем. В вашем примере s1 и s2 указывают на один и тот же объект, а сеттеры изменяют состояние одного и того же объекта, а изменения видны через ссылки.

Ответ 11

Измените свой класс, чтобы создать новую ссылку вместо того, чтобы использовать один и тот же:

public class SomeObject{

    public String text;

    public SomeObject(String text){
        this.setText(text);
    }

    public String getText(){
        return text;
    }   

    public void setText(String text){
        this.text = new String(text);
    }
}

Вы можете использовать что-то вроде этого (я не претендую на идеальное решение):

public class SomeObject{

    private String text;

    public SomeObject(String text){
        this.text = text;
    }

    public SomeObject(SomeObject object) {
        this.text = new String(object.getText());
    }

    public String getText(){
        return text;
    }   

    public void setText(String text){
        this.text = text;
    }
}

Использование:

SomeObject s1 = new SomeObject("first");
SomeObject s2 = new SomeObject(s1);
s2.setText("second");
System.out.println(s1.getText()); // first
System.out.println(s2.getText()); // second

Ответ 12

int a = new Integer(5) 

В вышеприведенном случае создается новое целое число. Здесь Integer является непримитивным типом и значение внутри него преобразуется (в int) и присваивается int 'a'.

SomeObject s1 = new SomeObject("first");  
SomeObject s2 = s1;

В этом случае оба s1 и s2 являются ссылочными типами. Они не созданы, чтобы содержать значение, подобное примитивным типам, скорее они содержат ссылку на некоторый объект. Для понимания мы можем рассматривать ссылку как ссылку, которая указывает, где я могу найти ссылку на объект.

Здесь ссылка s1 сообщает нам, где мы можем найти значение "first" (которое фактически хранится в памяти компьютера в экземпляре SomeObject). В других словах s1 - это адрес объекта класса "SomeObject", Следующим присваиванием -

SomeObject s2 = s1;

мы просто копируем значение, хранящееся в s1-s2, и теперь мы знаем, что s1 содержит адрес строки "first". После этого экземпляра println() производит тот же вывод, что и s1 и s2, переопределяя один и тот же объект.

Наряду с конструктором копирования вы можете скопировать объект с помощью метода clone(), если вы являетесь java пользователь. Клон можно использовать следующим образом:

SomeObject s3 = s1.clone(); 

Для получения дополнительной информации о clone() это полезная ссылка http://en.wikipedia.org/wiki/Clone_%28Java_method%29

Ответ 13

Вторая строка (SomeObject s2 = s1;) просто назначает вторую переменную первой. Это приводит к тому, что вторая переменная указывает на тот же экземпляр объекта, что и первый.

Ответ 14

Это потому, что s1 и s2 работают только как ссылки на ваши объекты. При назначении s2 = s1 вы назначаете только ссылку, что означает, что оба будут указывать на один и тот же объект в памяти (объект, который имеет текущий текст "первым" ).

Когда вы сделаете сеттер сейчас, либо на s1, либо на s2, оба будут изменять один и тот же объект.

Ответ 15

При назначении и объекте переменной вы действительно назначаете ссылку на этот объект. Таким образом, обе переменные s1 и s2 ссылаются на один и тот же объект.

Ответ 16

Так как объекты ссылались на ссылку. Поэтому, если вы пишете s2 = s1, будет скопирована только ссылка. Как и в C, вы только обрабатывали адреса памяти. И если вы скопируете адрес памяти и измените значение за этим адресом, оба указателя (ссылки) изменят одно значение в этой точке памяти.