Parameteröverföring och returvärden

Denna minilektion beskriver hur information överförs till och från metoder. Överföringen till metoder sker genom parametrar och överföringen från metoder genom returvärden.

Parameteröverföring

Parametrar används till föra över information från den som anropar en metod till själva metoden. Till skillnad från en del andra programmeringsspråk (t ex C++) finns det bara en mekanism för parameteröverföring i Java. Mekanismen kallas "värdeanrop" ("call by value"). Effekten är dock olika beroende på om parametern är av primitiv typ (int, double, char, ...) eller av referenstyp (array, String, Turtle, ...).

Värdeanrop innebär att värdet av argumentet (den aktuella parametern) dvs det som står i metodanropet först ska beräknas.

Antag t ex att vi har satsen

p.move((int)(Math.random()*10) + 4);

Argumentet dvs uttrycket (int)(Math.random()*10) + 4 kommer först att beräknas och resultera i ett heltalsvärde som, beroende på vad Math.random() returnerar, ligger mellan 4 och 14. Det resulterande värdet kopieras till den formella parametern (dvs den som är deklarerad i metodhuvudet). Därefter fungerar den formella parametern precis som en lokal variabel i metoden dvs den kan både refereras och ändras. Inget som görs med den parametern kan påverka argumentet.


Således:

En parameter är exakt som en lokal variabel som får ett startvärde vid själva metodanropet.

När parametern är en primitiv datatyp

Exempel: Givet följande metod metod:

void increase(int p) { System.out.println(" p in increase at entry: " + p); p = 2*p + 1; System.out.println(" p in increase at exit : " + p); }
Antag att metoden används av följande kod:
public static void main(String[] args ) int p = 1; System.out.println("p in main before call : " + p); increase(p); System.out.println("p in main after first call : " + p); increase(5*p+10); System.out.println("p in main after second call : " + p); }
Koden ger följande utskrifter:
p in main before call : 1 p in increase at entry: 1 p in increase at exit : 3 p in main after first call : 1 p in increase at entry: 15 p in increase at exit : 31 p in main after second call : 1

Processen vid det första anropet till increase kan illustreras nedan. Rutorna till höger innehåller koden utan utskriftssatser. Den röda pilen visar var exekveringen är.

Före första anropet ser det ut så här: Bild
public static void main(...) int p = 1; increase(p); increase(5*p+10); } void increase(int p) { p = 2*p + 1; }
När vi kommit in i increase ser det ut så här. Den tjocka, röda pilen visar överföringen av parametervärdet. bild
public static void main(...) int p = 1; increase(p); increase(5*p+10); } void increase(int p) { p = 2*p + 1; }
Vid utgången av increase ser det ut så här: bild
public static void main(...) int p = 1; increase(p); increase(5*p+10); } void increase(int p) { p = 2*p + 1; }
När vi lämnat increase så ser det ut som före anropet. bild
public static void main(...) int p = 1; increase(p); increase(5*p+10); } void increase(int p) { p = 2*p + 1; }

Slutsats:
Ingenting som vi gör med formella parametern påverkar argumentet (den aktuella parametern) eller något annat i den anropande koden.

En effekt av detta är att det inte går att skicka tillbaka någon information till anroparen med hjälp av sådana parametrar.

När parametern är en referenstyp

Samma mekanism används dvs den aktuella parameterns värde kopieras till den formella. Här måste man komma ihåg att parametern inte innehåller själva objektet utan är en referens (adress, pekare) till objektet. Det är således adressen som kopieras och inte objektet. Om metoden ändrar i objektet så har det alltså effekt i omvärlden.

Följande artificiella exempel avser illustrera detta

public class A { private int x = 1; private int y = 2; public String toString() { return "(" + x + ", " + y + ")"; } public void increase(A p, int i) { p.x += i; p.y += i; p = null; // Just for fun - no effect i++; // Just for fun - no effect } }

Vi nu lägger till och kör följande main-program i klassen:

Kod Utskrift
public static void main(String[] args) { A p = new A(); System.out.println("p before first call: " + p); increase(p, 3); System.out.println("p after first call : " + p); increase(p, 5); System.out.println("p after second call: " + p); } p before first call: (1, 2) p after first call : (4, 5) p after second call: (9, 10)

Nedan är en grafisk illustration av förloppet. Koden bantad på "ointressanta" delar.

Situationen när vi kommit in i increase för första gången. Bild public class A { private int x = 1; private int y = 2; public void increase(A p, int i) { p.x += i; p.y += i; p = null; // Just for fun i++; // Just for fun } } public static void main(...) { A p = new A(); increase(p, 3); increase(p, 5); }
Omedelbart före return från increase ser det ut så här:

Förändrade värden är inringade med rött.

Bild public class A { private int x = 1; private int y = 2; public void increase(A p, int i) { p.x += i; p.y += i; p = null; // Just for fun i++; // Just for fun } } public static void main(...) { A p = new A(); increase(p, 3); increase(p, 5); }

Observera att variablerna hör till (existerar i) en metod medan objektet inte gör det. Objekten ligger i en värld för sig och är åtkomliga för alla som har referenser till dem.

Arrayer

Eftersom arrayer är objekt och hanteras med referenser kan en metod som tar emot en array ändra i denna och detta har effekt i omvärlden som nästa exempel visar.

Antag att vi har följande metod som tar emot en array-referens som parameter och negerar värden i arrayen:

public negate(int[] a) { for (int i = 0; i < a.length; i++) { a[i] = -a[i]; } }

Antag att vi har följande kod (i main eller i någon annan metod):

int b[] = {1, 2, 3, 4}; negate(b);
När satsen
int b[] = {1, 2, 3, 4};
är utför ser det ut så här:
bild
När vi kommit in i negate men innan for-satsen påbörjats ser det ut så här: bild
Så här ser det ut vid slutet av negate: bild
När vi lämnat negate: bild

Instansvariabler som initieras med objekt från parametrar

Om man skickar en objektreferens till en klass och klassen sparar den i en instansvariabel uppstår en speciell situation. Även om instansvariabeln är deklarerad private kan alla som har en referens till objektet påverka detta.

Exempel: public class B { private String name; private int[] arr; public B(String name, int[] arr) { this.name = name; this.arr = arr; } }
Antag att vi har följande kod
t ex i main:
String n = "Kim"; int[] a = {1, 2, 3}; B b = new B(n, a);
Omdelbart före anropet av
konstruktorn ser det ut så här:

I objektvärden finns således ett
String-objekt och ett array-objekt.

bild 4a
String n = "Kim"; int[] a = {1, 2, 3}; B b = new B(n, a);
Omedelbart efter anropet av
konstruktorn ser det ut så här:

Nu har ett B-objekt skapats.

bild
String n = "Kim"; int[] a = {1, 2, 3}; B b = new B(n, a);

Med denna konstruktor har klassen B ingen kontroll över vad som händer med arrayen trots att instansvariabeln är deklarerad private. Alla i hela världen som har en referens till array-objektet kan ändra i det.

Om man vill garantera att någon utifrån inte kan peta på arrayen måste man i konstruktorn göra en kopia av den:
public B(String name, int[] arr) { this.name = name; this.arr = new int[arr.length]; for (int i = 0; i < arr.length; i++) { this.arr[i] = arr[i]; } }
Med denna konstruktor blir den avslutande bilden så här: bild 4c
Eftersom String-objekt är oföränderliga behöver inte name kopieras.

Om arrayparametern i sin tur innehåller objekt (t ex Turtle[]) så måste man kopiera även dessa för att vara säker på att ingen utifrån kommer åt dem. Detta kallas djup kopiering.

Observera också att det inte är säkert att man alltid vill göra djup kopiering. Om man t ex har objekt med personuppgifter vill man troligen inte manipulera kopior dessa (man vill t ex inte göra en adressändring i en kopia av ett personobjekt). Att bara kopiera referensen kallas grund kopiering.

Returvärden

Returvärden hanteras på ett liknande sätt: värdet av uttrycket som står i return-satsen kopieras ut till mottagaren. Om metoden returnerar en referens till ett objekt som också refereras av en instansvariabel måste man vara medveten om att objektet utelämnas till anroparens våld även om instansvariabeln är private.

Exempel: Antag att vi vill ha metoden int[] getArr() i klassen B ovan.

Två varianter:

public int[] getArr() { return this.arr; }
Lämnar ut instansvariabelns objekt till omvärldens godtycke (grund kopering).
eller
public int[] getArr() { int[] result = new int[arr.length]; for (int i = 0; i < arr.length; i++) { result[i] = arr[i]; } return result; }
Ett "säkert" utlämnande. Gör en kopia av arrayen och lämnar ut referensen till den. Instansvariabelns objekt fortfarande skyddat (djup kopiering).

Det är viktigt att förstå skillnaden mellan dessa. Vilken variant man använder beror på vad man vill uppnå.

En getName-metod kan riskfritt skrivas som

public String getName() { return this.name; }

eftersom String-objekt är oföränderliga.

Valid CSS!