Дублювання об'єктів у Java


74

Я дізнався, що коли ви змінюєте змінну в 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, якщо просте призначення не виконує цю роботу?


23
Ласкаво просимо до посилальних типів на Java, docstore.mik.ua/orelly/java-ent/jnut/ch02_10.htm .
Вікдор

5
Ласкаво просимо до stackoverflow. це основи Java .. спершу слід пройти концепцію Java .. скористайтеся цим підручником ..
pratikabu

2
Я думаю, що приклад заплутаний, навіть незважаючи на те, що обране рішення є правильним: Integerце тип, Objectа не примітивний. Якщо ви пишете Integer b = a;, bпосилається на той самий екземпляр, що і a. Але при написанні b = b+b;b посилається на новий екземпляр, який створюється +оператором.
Вінс,

Це не актуально, але не викликайте переоцінюваний метод у вашому конструкторі stackoverflow.com/questions/3404301/… .
halex

Крім того, можливо, ви захочете оголосити цей текстовий атрибут String приватним, оскільки ви надаєте методи getter та setter для роботи з ним, тому ви приховуєте деталі реалізації та надаєте класу з кращою абстракцією (загалом кажучи, кращою практикою OO)
stoldark

Відповіді:


134

Кожна змінна в 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);

13
Посилання +1 та примітиви копіюються за значенням. Виникає плутанина, яку ви не розрізняєте між посиланням та об’єктом, на який посилається. ;)
Пітер Лорі

15
s1.clone()це іноді варіант.
Райан Амос,

3
Ви можете використовувати s1.clone () для копіювання об'єкта в s2, він не передасть посилання на s2, а скопіює дані s1 в s2.
Mayur Raiyani


4
Кожна змінна в Java зберігає значення . А оскільки класи є посилальними типами, значення в типі змінної класу є посиланням . Коли ви виконуєте завдання, як у першому твердженні у цій відповіді чи у питанні, ви копіюєте посилання за значенням . Ви нічого не копіюєте за посиланням.
Адам Робінсон,

40

Відповідь @ 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- Перший оператор створює цілий об'єкт із значенням 5. Потім, при присвоєнні його змінній a, цілий об'єкт буде знято з коробки та збережено aяк примітив.

Після створення об’єкта Integer і до призначення:

введіть тут опис зображення

Після призначення:

введіть тут опис зображення

int b = a;

2- Це просто прочитає значення, aа потім збереже його b.

(Об'єкт Integer тепер придатний для збору сміття, але на даний момент не обов'язково збирається сміття)

введіть тут опис зображення

b = b + b;

3- Це зчитує значення bдвічі, додає їх разом і розміщує нове значення в b.

введіть тут опис зображення


З іншої сторони:

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.

введіть тут опис зображення

SomeObject s2 = s1;

2- Це зробить орієнтири s2на об'єкт, на який s1вказує.

введіть тут опис зображення

s2.setText("second");

3- Коли ви використовуєте сеттери для посилання, це змінить об'єкт, на який вказує посилання.

введіть тут опис зображення

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

4- Обидва повинні друкуватися second, оскільки два посилання s1і s2посилаються на один і той же об'єкт (як показано на попередньому малюнку).


1
Класно, +1 для вашої відповіді. Хоча я хочу запропонувати деякі доповнення: 1) зображення можуть бути трохи меншими. 2) Ви повинні додати відповідь на інше його запитання (як дублювати SomeObject), можливо, також із зображеннями.
бримборій

16

Коли ви це робите

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

у вас є 2 посилання на один і той же об’єкт. Це означає, що який би об’єкт посилання ви не використовували, внесені вами зміни будуть видимими при використанні другого посилання.

Подумайте про це так: у вас у кімнаті один телевізор, але два пульти дистанційного керування: не має значення, яким пультом ви користуєтесь, ви все одно внесете зміни до того самого основного об’єкта (телевізора).


5
+1, мені подобається аналогія дистанційного керування телевізором. Я думаю, примітивними є телевізори без пульта, де вам потрібно фізично перейти до фактичного набору і змінити значення ... чи щось інше? :)
Алекс

10

Коли ви пишете:

SomeObject s1 = new SomeObject("first");

s1не є SomeObject. Це посилання на SomeObjectоб’єкт.

Отже, якщо ви призначаєте s2 до s1, ви просто призначаєте посилання / дескриптори, а базовий об'єкт однаковий. Це важливо в Java. Все має передане значення, але ви ніколи не передаєте об’єкти навколо - лише посилання на об’єкти.

Отже, коли ви призначаєте s1 = s2, а потім викликаєте метод, s2який змінює об'єкт, базовий об'єкт змінюється, і це видно, коли ви посилаєтесь на об'єкт через s1.

Це один з аргументів незмінності об'єктів. Роблячи об’єкти незмінними, вони не змінюватимуться під вами і, таким чином, поводитимуться більш передбачувано. Якщо ви хочете скопіювати об’єкт, найпростішим / найпрактичнішим методом є написання copy()методу, який просто створює нову версію та копіює над полями. Ви можете робити розумні копії, використовуючи серіалізацію / відображення тощо, але це, очевидно, складніше.


+1 за згадку про поняття незмінності. Це, поряд із концепцією посилань, є ключовим у цій дискусії.
Marvo

4

З десяти помилок, які роблять програмісти Java :

6 - Плутанина щодо передачі за значенням та передачі за посиланням

Це може бути неприємною проблемою для діагностики, тому що коли ви дивитесь на код, ви можете бути впевнені, що він передається за посиланням, але виявляєте, що він насправді передається за значенням. Java використовує і те, і інше, тому вам слід розуміти, коли ви передаєте значення, а коли передаєте посилання.

Коли ви передаєте функції примітивний тип даних, такий як char, int, float або double, тоді ви передаєте значення. Це означає, що копія типу даних дублюється і передається функції. Якщо функція вирішить змінити це значення, вона буде змінювати лише копію. Як тільки функція завершиться, і керування повернеться до функції, що повертається, "реальна" змінна буде недоторкана, і жодних змін не буде збережено. Якщо вам потрібно змінити примітивний тип даних, зробіть це поверненим значенням для функції або оберніть всередині об’єкта.

Оскільки intце примітивний тип, int b = a;є копією за значенням, що означає, що aі bє двома різними об'єктами, але з однаковим значенням.

SomeObject s2 = s1;make s1і s2дві посилання на один і той же об'єкт, тому, якщо ви модифікуєте одне, буде змінено і інше.

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

public class SomeObject{

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

    // your code

}

Потім використовуйте його так:

SomeObject s2 = new SomeObject(s1);

Посилання, яке ви дали з "Десяти помилок, які роблять програмісти Java", не відповідає цьому контексту, оскільки йдеться про примітиви, а не про посилання на об'єкти.
Дрона

2

У вашому коді s1і s2є один і той же об'єкт (ви створили лише один об'єкт за допомогою new), і ви дозволяєте s2вказувати на той самий об'єкт у наступному рядку. Тому при зміні textвона змінюється як у тому випадку, якщо ви посилаєтеся на значення через s1і s2.

+Оператор Цілі створює новий об'єкт, він не змінює існуючий об'єкт (так що додавання 5 + 5 не дає 5 нове значення 10 ...).


2

Це тому, що JVM зберігає вказівник на s1. Під час дзвінка s2 = s1ви в основному говорите, що s2покажчик (тобто адреса пам'яті) має таке саме значення, як і для s1. Оскільки вони обидва вказують на одне і те ж місце в пам'яті, вони представляють абсолютно одне і те ж.

У =оператор привласнює покажчику значення. Він не копіює об'єкт.

Клонування об’єктів - справа сама по собі складна. Кожен об'єкт має метод clone (), який ви можете спокусити використовувати, але він робить неглибоку копію (що в основному означає, що він робить копію об'єкта верхнього рівня, але будь-який об'єкт, що міститься в ньому, не клонується). Якщо ви хочете пограти з копіями об’єктів, неодмінно прочитайте Ефективну Java Джошуа Блоха .


2

Почнемо з вашого другого прикладу:

Це перше твердження присвоює новий об'єкт s1

SomeObject s1 = new SomeObject("first");

Коли ви виконуєте призначення у другому операторі ( SomeObject s2 = s1), ви говорите, s2щоб вказувати на той самий об’єкт, на який s1в даний момент вказує, тому у вас є два посилання на один і той же об’єкт.

Зверніть увагу, що ви не дублювали SomeObject, швидше дві змінні просто вказують на один і той же об'єкт. Отже, якщо ви модифікуєте s1або s2, ви фактично модифікуєте один і той же об’єкт (зверніть увагу, якщо ви зробили щось подібне, s2 = new SomeObject("second")вони тепер вказували б на різні об’єкти).

У першому прикладі, aі bпримітивні значення, тому зміна не впливатиме на інших.

Під капотом Java всі об'єкти працюють, використовуючи передачу за значенням. Для об'єктів ви передаєте "значення", яке ви передаєте - це місце розташування об'єкта в пам'яті (тому, схоже, це має подібний ефект передачі за посиланням). Примітиви поводяться по-різному і просто передають копію значення.


2

Наведені вище відповіді пояснюють поведінку, яку ви спостерігаєте.

У відповідь на "Також, як продублювати SomeObject, якщо просте призначення не виконує цю роботу?" - спробуйте шукати cloneable(це інтерфейс Java, який надає один із способів копіювати об'єкти) та ' copy constructors' (альтернативний та, можливо, кращий підхід)


2

Присвоєння об'єкту посилання не клонує ваш об'єкт. Посилання схожі на вказівники. Вони вказують на об'єкт, і коли викликаються операції, це робиться над об'єктом, на який вказує вказівник. У вашому прикладі s1 та s2 вказують на один і той самий об'єкт, а сетери змінюють стан того самого об'єкта, а зміни видно у посиланнях.


2

Змініть свій клас, щоб створити нове посилання замість того, щоб використовувати те саме:

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

2
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


1

Це тому, що s1і s2працюйте лише як посилання на ваші об’єкти. При призначенніs2 = s1 ви призначаєте лише посилання, що означає, що обидва вказуватимуть на один і той же об'єкт у пам'яті (об'єкт, що має поточний текст "першим").

Коли ви зараз робите сетер, або на s1, або на s2, обидва модифікують один і той же об’єкт.


1

Коли ви призначаєте змінну і об'єктуєте її, ви дійсно призначаєте посилання на цей об'єкт. Отже, обидві змінні s1і s2посилаються на один і той же об’єкт.


1

Оскільки на об'єкти посилалися посилання. Отже, якщо ви пишете s2 = s1, буде скопійовано лише посилання. Це як у C, якщо ви обробляли лише адреси пам'яті. І якщо ви скопіюєте адресу пам'яті і зміните значення за цією адресою, обидва покажчики (посилання) змінять одне значення в цей момент в пам'яті.


1

Другий рядок ( SomeObject s2 = s1;) просто призначає другу змінну першій. Це призводить до того, що друга змінна вказує на той самий екземпляр об'єкта, що і перша.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.