Java-Anfänger hat simple Frage zum Kopieren von Referenzdatentypen

shkoder

Newbie
Registriert
Sep. 2015
Beiträge
3
Hallo liebe Community.

Erst mal möchte ich sagen dass ich neu hier bin und euch mal alle begrüßen!

Ich hab mir über die letzten paar Wochen mal Java selbst ein wenig beigebracht, bin aber jetzt auf etwas gestoßen was ich mir selbst nicht erklären kann.

Also, folgender Code:

class IntKlasse {
public int a;
public IntKlasse (int a) {
this.a=a;
}
}

class RefIntKlasse {
public IntKlasse x;
public double y;
public RefIntKlasse (int u, int v) {
x=new IntKlasse(u);
y=v;
}
}

public class Fahrzeug {
public static void copy3 (RefIntKlasse f, RefIntKlasse g) {
g=f;
}

public static void main (String args []) {
RefIntKlasse p=new RefIntKlasse(5,7);
RefIntKlasse q=new RefIntKlasse(1,2);
copy3(p,q);
System.out.println("q.y= "+q.y);
System.out.println("p.y= "+p.y);
}
}


So, nach Anwendung dieser Copy3-Methode sollten doch sowohl p als auch q die gleichen Speicherbereiche referenzieren ergo die gleichen Werte für die Variablen liefern; Tun sie aber nicht?!?! die ausgabe obigen programmes ist
q.y= 2.0
p.y= 7.0

und ich verstehe nicht weshalb.

wenn ich die copy-methode weglasse und einfach in die main schreibe "p=q;" funktioniert es, aber das ist doch der selbe code wie in der methode???

erleuchtet mich!

LG
 
deine copy Methode funktioniert so nicht...

die gibt nichts an p oder q zurück daher kann sich da auch nix ändern... dein f = g danach aber nicht p = q

p = copy(q);

und dann mir Rückgabewert arbeiten
 
Zuletzt bearbeitet:
1. Code-Tags benutzen. Das kann und will so keiner lesen.

Code:
class IntKlasse {
  public int a;
  public IntKlasse (int a) {
    this.a=a;
  }
}

class RefIntKlasse {
  public IntKlasse x;
  public double y;
  public RefIntKlasse (int u, int v) {
    x=new IntKlasse(u);
    y=v;
  }
}

public class Fahrzeug {
  public static void copy3 (RefIntKlasse f, RefIntKlasse g) {
    g=f;
  }

  public static void main (String args []) {
    RefIntKlasse p=new RefIntKlasse(5,7);
    RefIntKlasse q=new RefIntKlasse(1,2);
    copy3(p,q);
    System.out.println("q.y= "+q.y);
    System.out.println("p.y= "+p.y);
  }
}

2. Die Erklärung: In Java werden immer(!) nur Pointer per "Pass-by-Value" übergeben. D.h. deine copy3 Methode erhält eine Kopie der Pointer auf p und q, also modifizierst du in deiner copy3 Methode nur die Kopie des Pointers, aber niemals den originalen Pointer.

Um das Problem zu umgehen, kann man Wrapper Klassen benutzen, deren Inhalt man dann modifiziert. Hier ein ungetesteter Code-Schnipsel zur Veranschaulichung:
Code:
public class Wrapper {
  public Object content;
  public Wrapper(Object content) {
    this.content = content;
  }
}

public class Application {
  public static void copy(Wrapper o1, Wrapper o2) {
    o1.content = o2.content;
  }

  public static void main(String... args) {
    Wrapper obj1 = new Wrapper(new Foo(1, 2));
    Wrapper obj2 = new Wrapper(new Foo(3, 4));
    copy(obj1, obj2);
    System.out.println(obj1.content);
    System.out.println(obj2.content);
  }
}
Hier zeigen am Ende sowohl obj1.content als auch obj1.content auf dasselbe Objekt Foo(3, 4).
 
Zuletzt bearbeitet:
benneque schrieb:
2. Die Erklärung: In Java werden immer(!) nur Pointer per "Pass-by-Value" übergeben. D.h. deine copy3 Methode erhält eine Kopie der Pointer auf p und q, also modifizierst du in deiner copy3 Methode nur die Kopie des Pointers, aber niemals den originalen Pointer.

Falsch. Nicht-Basisdatentypen (alle Typen, die kleingeschrieben sind (short, float, int, double, long, boolean)) werden kopiert (call by value), alles andere wird mittels Objektreferenz übergeben (call by reference). Das Konzept der "pointer" gibt es so nicht in Java. Man hat immer eine Objektreferenz.

Copy-Konstruktoren oder Methoden zur Kopie von Instanzen in Java sind immer schlechter Stil. Dafür gibt es Object.clone.
 
Zuletzt bearbeitet:
@benneque:

danke für die erklärung! also eine methode bearbeitet quasi immer nur kopien der übergebenen argumente?

wie kann ich dann eigentlich prinzipiell mit einer methode überhaupt dauerhaft was im gesamten programm verändern? weil wenn ich zB in der methode die einzelnan variablen kopieren lasse geht es!
Ergänzung ()

@preposterous: danke! das ergänzt den vorigen post schon sinnvoller!

wieso funktioniert dann aber zB in meinem bsp die methode

public static void copy2 (RefIntKlasse f, RefIntKlasse g){
g.x=f.x;
g.y=f.y;
}

???

x ist ja wieder eine referenzvariable und damit dürfte die neue zuweisung(deiner erklärung nach) nicht greifen oder?

LG
 
preposterous schrieb:
Aha. Und nur weil du dir Dinge einbildest, sollte ich nun Unrecht haben?
http://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
http://www.nosid.org/java-call-by-value-reference.html
http://javadude.com/articles/passbyvalue.htm

Es ist IMMER Pass-by-Value! Und ob du die Dinger nun Pointer (C/C++) oder References (Java) nennst, ist doch wirklich Jacke wie Hose: Es ist und bleibt eine Speicheradresse. Und ob man den übergebenen Wert als Primitive oder Objekt betrachtet ist auch vollkommen egal, weil das Verhalten identisch ist.

Was ich meine:
Code:
public static void modify(int i, String s) {
  		i++;
  		i = 5;
  		s = "bar";
	}

	public static void main(String[] args) {
  		int i = 1;
  		String s = "foo";
  		modify(i, s);
  		System.out.println(i);
  		System.out.println(s);
	}
Wir sind uns hoffentlich einig, dass i ein Primitive und s ein Objekt ist. Und nun behandle die Methodenaufruf in beiden Fällen identisch: Es wird jeweils eine Kopie des Pointer auf die Variable übergeben. Funktioniert tadellos, weil Primitives (und deren Autoboxing Objekt Gegenstücke) Immutable sind. Wozu soll man sich 2 Konzepte merken, wenn eines reicht?


preposterous schrieb:
Copy-Konstruktoren oder Methoden zur Kopie von Instanzen in Java sind immer schlechter Stil. Dafür gibt es Object.clone.
Und noch so eine Unwahrheit. Grandios. Und dafür hast du dich extra angemeldet? Shame on you!
http://stackoverflow.com/questions/2427883/clone-vs-copy-constructor-which-is-recommended-in-java
http://www.javapractices.com/topic/TopicAction.do?Id=12
http://www.javacodegeeks.com/2014/01/which-is-better-option-cloning-or-copy-constructors.html

Es gibt quasi einen Fall, in dem die Clone Methode sehr gut funktioniert: Wenn das Objekt keine Referenzen enthält bzw. wenn das gesamte(!) Objekt immutable ist - also inkl. all seiner Referenzen.





shkoder schrieb:
x ist ja wieder eine referenzvariable und damit dürfte die neue zuweisung(deiner erklärung nach) nicht greifen oder?

Ich versuch's dir mal am Speicher zu zeigen. Alle Variablen zeigen auf ein Speicheradresse:
Code:
public static void copy2 (RefIntKlasse f, RefIntKlasse g){ // f und g sind die übergebenen
      // Speicheradressen "1" und "4"
      g.x=f.x; // hier wird also das "x" von Speicheradresse "4" mit dem "x" von Speicheradresse "1" überschrieben
      // das Ergebnis ist, dass das "x" von Speicheradresse "4" nun auf dieselbe Speicheradresse wie das "x" von "1" zeigt
      g.y=f.y; // hier analog dasselbe mit "y"
}

public static void main(String...args) {  // hier beginnt das Programm
    RefIntKlasse a = new RefIntKlasse(1,2); // a zeigt auf Speicheradresse "1", innerhalb von a
    // sind auch noch mal 2 Variablen, die bekommen natürlich auch ein Speicheradresse zugewiesen:
    // a.x zeigt auf Speicheradresse "2", a.y zeigt auf Speicheradresse "3"
    RefIntKlasse b = new RefIntKlasse(3,4); // b zeigt auf Speicheradresse "4", und
    // b.x zeigt auf Speicheradresse "5", b.y zeigt auf Speicheradresse "6"
    copy(a,b); // hier werden also die Adressen "1" und "4" übergeben
   
    // nach dem Aufruf, zeigen an dieser Stelle im Programm "a" und "b" immernoch auf
    // ihre alten Speicherbereiche "1" und "4", allerdings zeigt b.x jetzt auf "2" und b.y auf "3"
}

Eventuell macht es das hier noch deutlicher. So kann man es sich (zumindest für Java) ziemlich einfach vorstellen (wie der Computer das intern macht, kann einem in Java ziemlich egal sein, dank der JVM). Ich habe die Speicheradressen in Klammern mit angegeben, damit deutlich wird, wann es dieselbe Adresse und wann eine andere bzw. eine neue ist.
Code:
y = 1; // y zeigt auf eine Speicheradresse(8) mit dem Inhalt "1"
x = y; // x zeigt nun auf dieselbe Speicheradresse(8) wie y
y = 2; // y zeigt nun auf eine Speicheradresse(12) mit dem Inhalt "2"
// aber es wurde nirgends der Inhalt der Speicheradresse geändert!
// und x zeigt auch immer noch auf die alte Speicheradresse(8)
x = y; // x zeigt wieder auf dieselbe Speicheradresse(12) wie y
x = 3; // x zeigt nun auf eine Speicheradresse(15) mit dem Inhalt "3"

Dasselbe gilt für Objekte:
Code:
y = new RefIntKlasse(1, 2); // "new" allokiert neuen Speicher für das neue Objekt
// und y zeigt nun auf diese neue Speicheradresse(5)
x = y; // x zeigt nun auf dieselbe Speicheradresse(5) wie y
y = new RefIntKlasse(3, 4); // y zeigt nun auf eine neue Speicheradresse(9)
// aber es wurde nirgends der Inhalt der Speicheradresse geändert!
// und x zeigt auch immer noch auf die alte Speicheradresse(5)
x = y; // x zeigt wieder auf dieselbe Speicheradresse(9) wie y
x = new RefIntKlasse(5, 6); // x zeigt nun auf eine neue Speicheradresse(11)

Und auch für Objekte mit enthaltenen Primitives:
Code:
y = new RefIntKlasse(1, 2); // "new" allokiert neuen Speicher für das neue Objekt
// und y zeigt nun auf diese neue Speicheradresse
x = y; // x zeigt nun auf dieselbe Speicheradresse wie y
x.a = 4; // ändert den a-Wert des von x referenzierten Objekts
// das ist aktuell dasselbe Objekt, das auch von y referenziert wird
// d.h. x.a == y.a (selbe Speicheradresse)
y = new RefIntKlasse(3, 4); // y zeigt nun auf eine neue Speicheradresse
y.a = 6; // ändert den a-Wert des von y referenzierten Objekts
// y zeigt immer noch auf die alte Speicheradresse
// daher zeigen x.a und y.a auf unterschiedliche Speicheradressen

x.a = y.a; // x und y zeigen natürlich weiterhin auf unterschiedliche
// Speicheradressen, aber x.a und y.a zeigen nun auf dieselbe, d.h. x.a == y.a
y.a = 42; // ändert die Speicheradresse auf die y.a zeigt
// das ändert aber nichts an x oder x.a
 
Zuletzt bearbeitet:
ok benneque, vielen vielen dank, ich glaube es hat klick gemacht!

kurzum: java arbeitet in methoden IMMER mit kopien der übergebenen objekte (pass-by-value), allerdings hat das bei referenztypen zT andere auswirkungen als bei primitiven typen, weil objekte "hinter der referenz" sozusagen trotzdem dauerhaft verändert werden (weil NUR die referenz in der methode kopiert wird).

kann man es so stehen lassen?

PS: vielen dank für die mühe die du dir gemacht hast, echt sehr nett!
 
Man kann es so stehen lassen, wenn du es verstanden hast ;)

In Java gibt es ja generell 2 Arten von Datentypen: Primitives (int, long, boolean, ...) und Objekte. Und für jedes Primitive gibt es auch ein entsprechendes Objekt (Integer, Long, Boolean, ...). Intern macht Java keinen wirklichen Unterschied zwischen "int" und "Integer" (oder boolean und Boolean, etc. pp.), daher auch meine Aussage, das man für's Verständnis ruhig alles in einen Topf schmeißen kann. (Mit Java 9 oder 10 könnte sich das allerdings ändern, zumindest war irgendwo im Gespräch, dass es neue Datentypen geben könnte - sogenannte "Value Objects", aber bis dahin .... ;) )

Wenn du nun eine Methode mit einem "int" aufrufst, kannst du dir also genau so gut vorstellen, dass es ein "Integer" ist. Also stellst du dir entweder vor, dass du den Wert(!) eines "int" kopierst und übergibst, oder dass du eine Kopie des Pointers auf den "Integer" übergibst. Das Ergebnis ist dasselbe: Du kannst das worauf die Variable vor dem Methodenaufruf gezeigt hat innerhalb der aufgerufenen Methode nicht ändern (oder für die Primitives: Du kannst den Wert den die Variable vor dem Methodenaufruf hatte innerhalb der aufgerufenen Methode nicht ändern).

Das ist wie "int x = 4" und "Integer y = new Integer(4)". In Java kannst du beide Variablen vergleichen mit "x == y" und es kommt true raus, weil beide Datentypen intern vollkommen gleich behandelt werden.
Meiner Meinung nach sollte man Primitives nicht gesondert behandeln. Aber es gibt natürlich Ausnahmen: Primitives können nicht "null" sein. D.h. "int x = null" ist ungültiger Code, aber "Integer y = null" ist valide. Und "Integer" (und die ganzen anderen Primitive-Wrapper) haben natürlich diverse Methoden, wohingegen die Primitives selbst nichts mitbringen.
Eine weitere Ausnahme ist der "==" Vergleichsoperator. Dieser vergleicht bei Primitives den Wert und bei Objekten die Referenz. Bei "int == Integer" wird der Integer intern zum "int" gemacht und die Werte verglichen. Bei "Integer == Integer" werden die Referenzen verglichen, d.h. "new Integer(4) == new Integer(4)" ist false, weil beides eigenständige Objekte sind, die auf eigene Speicherbereiche zeigen. Das sind die Eigenheiten des Autoboxing, aber solche Fälle trifft man aber (fast) nie an.
(Da gibt's für die Wrapper Klassen, dann noch die statische "valueOf" Methode, die man statt des Konstruktors benutzen kann. Dann erhält man das übliche Primitive Verhalten. Aber wie gesagt: Kommt (so gut wie) nie vor, dass man das braucht. Ich hatte in 15 Jahren Java vielleicht 2-3 mal damit zu tun)

Primitives sollten (Konjunktiv!) etwas leichter und performanter sein, was für den normalen Programmierer aber völlig unerheblich sein sollte und zusätzlich sitzt noch die JVM mit Jit-Compiler zwischen Java-Code und Ausführung, um den Code bei Bedarf zu verändern und zu optimieren.
Wenn du irgendwann zu den Generics und Collections kommst, wirst du wieder auf einen Unterschied stoßen: Primitives lassen sich hier gar nicht benutzen.
 
Zuletzt bearbeitet:
benneque schrieb:
In Java gibt es ja generell 2 Arten von Datentypen: Primitives (int, long, boolean, ...) und Objekte.
Was man übrigens schon fast als Verbrechen bezeichnen kann. Also wenn eine Sprache eh schon sehr auf OOP setzt, dann kann man auch gleich alles zu Objekten machen und nicht noch differenzieren.

Gruß
Andy
 
andy_m4 schrieb:
Was man übrigens schon fast als Verbrechen bezeichnen kann. Also wenn eine Sprache eh schon sehr auf OOP setzt, dann kann man auch gleich alles zu Objekten machen und nicht noch differenzieren.

Gruß
Andy

Das erinnert mich irgendwie an Javascript, hat auch super geklappt. Herum gekommen ist so eine Art Pseudoklassenvererbungsding ^^
 
Das erinnert mich irgendwie an Javascript, hat auch super geklappt. Herum gekommen ist so eine Art Pseudoklassenvererbungsding ^^
Naja. Das Prototypen-Konzept gab es ja vorher schon in Self. Und manchmal ist es schon ganz praktisch. Allerdings hat Javascript natürlich auch viele andere Baustellen.

Ein Klassenkonzept a-la Java kommt ja nun mit dem neuen Standard bzw. kann man ja mit Microsofts TypeScript das schreiben und ins "alte" Javascript crosscompilen..

Gruß
Andy
 
shkoder schrieb:
kurzum: java arbeitet in methoden IMMER mit kopien der übergebenen objekte (pass-by-value), allerdings hat das bei referenztypen zT andere auswirkungen als bei primitiven typen, weil objekte "hinter der referenz" sozusagen trotzdem dauerhaft verändert werden (weil NUR die referenz in der methode kopiert wird).

kann man es so stehen lassen?

PS: vielen dank für die mühe die du dir gemacht hast, echt sehr nett!

Nein das stimmt nicht es wird nur die Referenz kopiert bei Objekten. Deswegen sind in deinen Beispielen Änderung an der Referenz außerhalb der Methode nicht sichtbar aber Änderung am Objekt schon
Ergänzung ()

benneque schrieb:
Wenn du nun eine Methode mit einem "int" aufrufst, kannst du dir also genau so gut vorstellen, dass es ein "Integer" ist. Also stellst du dir entweder vor, dass du den Wert(!) eines "int" kopierst und übergibst, oder dass du eine Kopie des Pointers auf den "Integer" übergibst. Das Ergebnis ist dasselbe: Du kannst das worauf die Variable vor dem Methodenaufruf gezeigt hat innerhalb der aufgerufenen Methode nicht ändern (oder für die Primitives: Du kannst den Wert den die Variable vor dem Methodenaufruf hatte innerhalb der aufgerufenen Methode nicht ändern).
Natürlich kann man den Wert des Integers in einer Methode verändern!
 
Und wie? Jetzt mal abgesehen von irgendwelchen "Hacks" über Reflection, Native-Code, AspectJ, etc. pp.

Code:
public void foo(int x, Integer y) {
    x++;
    y++;
}

public void main(String... args) {
   int x = 1;
   Integer y = new Integer(1);
   foo(x, y);
   // x und y sind immer noch 1
}

Mit 'nem AtomicInteger geht's natürlich, aber das ist was ganz anderes.
 
Integer war wohl ein schlechtes Beispiel wegen unboxing.

Code:
public static void change(MyObject x){
  x.x += 5;
  x.y += 5;
}

public static void main(String[] args){
  MyObject myInt = new MyObject(5);
  System.out.println(myInt.x+":"+myInt.y);
  change(myInt);
  System.out.println(myInt.x+":"+myInt.y);
}
}

class MyObject{
  int x;
  int y;

  MyObject(int blah){
    x = blah;
    y = blah;
  }
}
führt zu der Ausgabe
Code:
5:5
10:10
 
Wie ich schon schrieb: "Du kannst das worauf die Variable vor dem Methodenaufruf gezeigt hat innerhalb der aufgerufenen Methode nicht ändern".

Dein myInt ist und bleibt vor und nach dem Methodenaufruf ein- und dasselbe Objekt: myInt zeigt vor und nach dem Aufruf auf denselben Speicherbereich. Du kannst also innerhalb einer Methode nicht das übergebene Objekt überschreiben, ohne dass der aufrufende Code das explizit will, z.B. "x = foo(x);". Dass man die Interna eines übergebenen Objekts verändern kann, ist selbstverständlich.
 
Zurück
Oben