Java programozás 22. – Saját metódusok, avagy ne ismételd önmagad

Saját metódusok, avagy ne ismételd önmagad

Ezzel a leckével gyakorlatilag kibővítem az alap leckékben szereplő Metódusok témakört. A programozási nyelv készítői, mint már említettem, nagyon sok okos dolgot előre megírtak, amelyeket használhatunk. Math osztály, emlékszel? A helyzet az, hogy ők csak a legfontosabbakat írták meg, amelyekre sokaknak szükségük van. Mi van akkor, ha van egy olyan részfeladatunk, amelyet többször is végre szeretnénk hajtani egy feladat megoldása során. Gondoljunk csak arra, hogy egy tömbbel különféle műveleteket szeretnénk végezni, és a tömb elemeit többször is ki szeretnénk íratni ellenőrzés céljából. Minek írjuk meg sokszor ugyanazt a kódot, ami ezt elvégzi? És ha változtatni szeretnénk a kiíratáson? Nem egymás alá, hanem egymás mellé szeretnénk az elemeket kiírni. Mindenhol átírjuk egyesével a println-t print-re? Logikusnak tűnik, hogy ezt valószínűleg hatékonyabban is megtehetjük.

Eddig olyan programokat készítettünk, amelyek valamilyen adatokkal különféle feladatokat oldottak meg. Ha sorban haladtál a tananyagon, akkor még nem írtunk saját objektumokat, maximum fogalom szinten találkoztunk vele. Mivel előfordulhat, hogy valaki ennek a leckének az anyagára kíváncsi, fontosnak tartom rögzíteni a következőt:

Ebben a leckében csak olyan metódusokkal foglalkozok, amelyeket egy olyan programban készítünk el, amelyik nem tartalmaz saját objektumokat, és a programunk utasításait a main metódusba írjuk. Ennek megfelelően a címet is módosítom:

Saját metódusok main metódus mellé

Metódusok típusai

Amikor saját metódust akarunk írni, akkor valamilyen rész feladatot szeretnénk kiemelni a kódunkból egy különálló egységbe. Ezt megtehetjük azért, hogy a kód bonyolultságát csökkentsük, ezért a feladat egy részét külön választjuk. A gyakoribb ok azonban az, hogy valamilyen ismétlődő részfeladatot akarunk kiemelni, hogy ne kelljen többször megírni. Ilyenkor javítani, változtatni is egyszerűbb ezen, mivel a sok helyen használatos kódot egyszer írtuk meg, külön kiemelve, a többi helyen csak hivatkoztunk rá.

A metódusba kiemelt kódokat formailag meglehetősen kötött módon kell megírni. A Metódusok leckében azt mondtam, hogy a metódusok egyfajta üzenetek, melyek a különféle objektumok közötti kommunikációt (adatok lekérdezése, feladatok végrehajtásának kérése) valósítják meg. Jelenleg a programjainkban saját objektumokkal még nem dolgozunk. A metódusok csak a programunk fő részét alkotó main metódust egészítik ki. Ezeket a kódrészeket a main elé vagy után kell megírni. (Ez más programnyelvekben nem ennyire rugalmas.)

Metódusokat alapvetően kétféle okból készítünk:

  1. Valamilyen feladatot szeretnénk vele megoldani úgy, hogy a feladatnak nincs visszaadott eredménye.
  2. Valamilyen feladatot szeretnénk vele megoldani, és a feladatnak végeredménye is lesz.

Az első eset lehetne egy kiíratás, ahol a feladat csak annyi, hogy a képernyőn megjelenjen valami. A második esetben valamilyen számítási műveletet hajtunk végre a metódussal, aminek a célja, hogy a metódus egy eredményt állítson elő. Nagyon fontos a mondatban az egy szó, mert a metódus csakis egyetlen eredményt adhat vissza. Ez az egyetlen visszaadott eredmény a visszatérési érték.

Két konkrét példával élve az első eset legyen az, hogy néha ki kell rajzolnunk egy fix hosszúságú vízszintes vonalat a képernyőre, a második esetben pedig tegyük fel, hogy a programunknak néha szüksége lesz egy véletlen számra egy adott intervallumból. Nem célszerű a vonal rajzolást vagy véletlen szám sorsolást újra és újra megírni, ezért készítünk egy-egy metódust, aminek annyi a feladata, hogy ha kiadjuk neki a parancsot, végezze el ezeket a feladatokat. A két példában felsorolt metódusok minden esetben ugyanazon belső utasításaik szerint végzik el a feladatukat, nem tudom befolyásolni a működésüket. Hiszen mindig ugyanolyan vonalat akarok rajzoltatni, és mindig adott intervallumból kell véletlen szám.

Ahhoz, hogy a metódusok működésére hatással legyek, a metódusoknak bemenő adatokat is kapniuk kell, ami megváltoztatja a belső utasítások végrehajtását. Ezeket a bemenő adatok a paraméterek (változók). Tegyük fel nem csak egy vonalat szeretnénk kirajzolni – jelekből, hanem a kiíratással egy tömb elemeit szeretném megjeleníteni a képernyőn. Vagy nem mindig ugyanabból az intervallumból kérek véletlen számot, hanem odaadom a feladatot elvégző metódusnak az intervallum határait (alsó-felső), amiből sorsolnia kell. A metódusnak odaadott paraméterek száma tetszőleges lehet. Ha így nézzük a dolgot, akkor a már négyféle alkalmazási módnál járunk:

  1. Egy feladat végrehajtását kérjük tőle visszatérési érték nélkül, paraméterek nélkül.
  2. Egy feladat végrehajtását kérjük tőle visszatérési érték nélkül, paraméterek segítségével.
  3. Egy visszatérési értéket kérünk tőle paraméterek nélkül.
  4. Egy visszatérési értéket tőle paraméterek segítségével.

Lássunk akkor példákat a fent felsorolt alkalmazási módra:

  1. Szeretnénk egy metódust készíteni, ami kirajzol egy — jelekből álló vonalat az egyes feladatok elválasztására
public static void vonal()
{
  System.out.println("-----");
}
  1. Szeretnénk egy metódust készíteni, mely egy kapott tömb elemeit kiírja a képernyőre.
public static void kiir( int[] tomb )
{
  for( int i = 0; i < tomb.length; i++ )
  {
    System.out.println(tomb[i]+" ");
  }
  System.out.println();
}
  1. Szeretnénk egy metódust készíteni, amely visszaad egy véletlen számot a [-10;30] intervallumból.
public static int veletlen()
{
  int szam;
  szam = (int)(Math.random()*41)-10;

  return szam;
}
  1. Szeretnénk egy metódust készíteni, amely két kapott szám átlagát adja vissza eredményként.
public static double atlag( int szam1, int szam2 )
{

  double atl = (szam1+szam2 )/2.0;

  return atl;
}

Ez a négy példa alapvetően lefedi a metódusok alaptípusait. Láthatod, hogy a kapott adatok darabszáma tetszőleges, a 2. példában egyetlen adatot kap a metódus (a tömböt, aminek az elemeit ki kell írnia), a 4. példában pedig a két számot, amelyek átlaga lesz a visszaadott eredmény. Több adatot is adhatok neki, de emlékezz rá: minden esetben egyetlen visszatérési érték kell.

Metódusok részei

A metódust alapvetően két részre bonthatjuk, fejre és törzsre, valamint a fej még tovább bontható. Lássuk mi micsoda:

A metódusok különféle részeit a fent felsorolt példákon keresztül mutatnám be:

  • public: A metódusoknál különféle módosítókat, például hozzáférési szintet adhatunk meg, hogy az adott metódust milyen kód használhatja. Ebbe most részletesen nem mennék bele, egyelőre fogadjuk el, hogy a public olyan hozzáférést biztosít, hogy akármilyen kód használhatja ezt a metódust. A public kulcsszó akár el is hagyható, de a hiánya is egyfajta jogosultsági szintet jelent. Egyéb módosítók is léteznek, ezekről későbbi leckében írok.
  • static: Kötelező, ha a main-ben megírt kódjainkat szeretnénk kiegészíteni a metódusokkal. Nélküle a kód szintaktikai hibát tartalmaz. A saját objektumokba írt metódusoknál nagyon nem mindegy, hogy használjuk-e a static kulcsszót, vagy sem. Erről szintén egy későbbi lecke szól majd.
  • visszatérési érték típusa: A static után meg kell adni, hogy a metódusnak van-e visszatérési értéke, vagy nincs. Ha van, akkor a visszatérési érték típusát kell megadni (3-4 példák), ha nincs, akkor a void kulcsszót kell használni (1-2 példák). Az ábrán vörös nyíllal kiemeltem, hogy ezeknek a típusoknak kötelezően egyeznie kell!
  • metódus neve: A metódus neve a névadási szabályoknak megfelelően bármi lehet, de illik olyan nevet adni, ami utal arra, hogy a metódus milyen feladatot lát el. A név jellemzően egyedi, de bizonyos körülmények együttállása esetén azonos nevek is megengedettek, ezekről a felsorolás után írok. A metódusok nevénél is illik használni a camelCase elvet, vagyis több szóból álló nevek esetén az első szó kisbetűvel kezdődik, utána minden szó kezdőbetűje nagybetű.
  • zárójelek: Kötelező formai elemek, de csak akkor kell szerepeltetni benne valamit, ha a metódus paramétert vagy paramétereket kap.
  • paraméterek: Amennyiben szükség van rájuk, minden paramétert típussal és névvel kell megadni a zárójelen belül, vesszőkkel elválasztva. A paraméter típusának a hozzátartozó értékhez passzolnia kell, neve a névadási szabályoknak megfelelően bármi lehet. A kapott paraméter hatóköre a metódus blokkjára korlátozódik, csak a metóduson belül értelmezett. A változó értékét csak a metódusban lehet használni. Más metódusokban is használhatjuk ugyanezt a változónevet, ezek között nincs átfedés, nincs hatásuk egymásra. A kapott paraméter neve meg is egyezhet a metódus nevével, de szerintem az egyezést célszerű kerülni.
  • blokk: A metódusnak egy speciális esetet leszámítva kötelező blokkot nyitni. A blokkba írjuk meg azokat az utasításokat, amelyek a feladatot végrehajtják, vagy előállítják az eredményt.
  • return: Amennyiben a metódusnak van visszatérési értéke, a blokknak mindenképpen tartalmaznia kell legalább egy return szóval kezdődő utasítást, ami után szerepelnie kell annak a változónak, értéknek, kifejezésnek, amely az eredményt határozza meg. Ha a blokkon belül több feltételhez kötött ág is található, biztosítani kell, hogy return utasítással mindenképpen találkozzon a végrehajtás. Visszatérési érték nélküli (void) típusú metódus esetén is használható return utasítás. Ilyenkor a return után csak a lezáró pontosvessző állhat. A szerepe az, hogy a metódus végrehajtása azonnal véget ér, és a return utáni utasítások végrehajtása nem történik meg. A vezérlés ebben az esetben azonnal visszakerül a metódust hívó utasításhoz.

Metódus szignatúrája

A metódus neve, a paraméterek típusai, darabszáma, és azok sorrendje együttesen a metódus szignatúrája. A metódusoknak csak a szignatúrája kell, hogy egyedi legyen! Az adott kódban tehát létezhetnek azonos nevű metódusok, de csak akkor, ha ezek szignatúrája különbözik. Ezt nevezzük a metódus túlterhelésének. Lássunk erre néhány példát:

public static void vonal()
{
  System.out.println("----------");
}

public static void vonal( int szam )
{
  for( int i = 0; i < szam; i++ )
  {
    System.out.print("-");
  }
  System.out.println();
}

public static void vonal( int szam, char c )
{
  for( int i = 0; i < szam; i++ )
  {
    System.out.print(c);
  }
  System.out.println();
}

public static void vonal( char c, int szam )
{
  for( int i = 0; i < szam; i++ )
  {
    System.out.print(c);
  }
  System.out.println();
}

A négy metódus azonos néven szerepel, de az elsőnek nincs paramétere, a második egy számot kap, ami meghatározza, hogy milyen hosszú vonalat rajzoljon ki a képernyőre, a harmadik megkapja a vonal hosszát, és hogy milyen karakterekből rajzolja ki a vonalat. A negyedik metódus kakukktojás. Látszik, hogy ugyanazt a feladatot végzi el, mint a harmadik metódus, a paraméterei darabszámra és típusra is megegyeznek. Egyetlen dologban különböznek, a paraméterek sorrendjében. Ez szignatúra szempontjából különbség, ennek megfelelően a két utolsó metódus létezhet egymás mellett, még akkor is, ha sok értelme ebben a formában nincs. A szignatúra nem tartalmazza a változók neveit. Ennek megfelelően adjuk meg a fenti metódusok szignatúráját:

vonal()
vonal( int )
vonal( int, char )
vonal( char, int )

Ha a változóneveket a definíciónak megfelelően figyelmen kívül hagyjuk, ilyen metódusok nem létezhetnek egymás mellett:

public static void vonal( int szam, char c)
{
  ...
}

public static void vonal( int hossz, char mibol )
{
  ...
}

Hiába mások a paraméterek nevei, ha azok típusra, darabszámra és sorrendre megegyeznek, a metódusok nevei is ugyanazok, vagyis szignatúra szerint nem különböznek.

public static void vonal( int hossz, String mibol )
{
  ...
}

Az fenti példa viszont különbözik a két előzőtől. Hiába egyezik meg az előző második példával a metódus neve, paraméterek darabszáma, sorrendje, még a neve is, ha a mibol paraméter típusa eltérő. Az más kérdés, hogy kiíratás szempontjából majdnem mindegy, hogy karakterekből vagy Stringek-ből rajzolom ki a vonalat. (Valójában azért jelentős különbség van közöttük). Akkor is más a szignatúra, vagyis létezhetnek egymás mellett.

Metódusok hívása

Metódusok használata nem kötelező, de sok esetben kétségtelenül hatékonyabb vagy átláthatóbb a programunk, ha alkalmazzuk őket. Nézzük hogyan használjuk őket. A metódusok használatát hívásnak nevezzük. A hívás első lépése a metódus megszólítása. A metódusra a nevével hivatkozunk, aminek célszerű egyedinek lennie. Megszólítjuk a neve alapján a kívánt metódust, valamint ha vannak paraméterei, akkor típushelyesen odaadjuk neki a szignatúrának megfelelő sorrendben. Adhatunk neki változókat, direkt értékeket, vagy kifejezéseket is. Ha egy metódus két egész számot kér, akkor odaadhatunk neki két egész változót (a benne lévő értékeket), megadhatunk két általunk megadott számot, vagy akár keverhetjük is a kettőt, odaadunk egy számot és egy változót. A lényeg a típushelyesség és a sorrend.

Lássunk akkor példákat metódus hívásokra az alkalmazási módoknál felsorolt példáknak megfelelően:

  1. Ki szeretnék íratni egy 5 egység hosszú vonalat – jelekből a megírt metódus használatával:
vonal();
  1. Ki szeretném írni egy tömb elemeit a képernyőre egymás mellé szóközökkel elválasztva:
kiir(tomb);
  1. Szeretnék egy véletlen számot kisorsolni a [-10;30] intervallumból.
int szam = veletlen(); // sorsolom és eltárolom
System.out.println( veletlen() ); // sorsolom és kiírom
  1. Ki szeretném számolni két szám átlagát.
int szam = 3;
double atlag = atlag( 4, szam );

Láthatod, hogy a példákban gyakorlatilag csak a szignatúra szerepel. meg kell adni a metódus nevét, és oda kell neki adni a működéséhez szükséges adatokat direkt értékek, változók vagy kifejezések formájában. Amikor a metódusnak nincs visszatérési értéke (void típusú), akkor a metódus hívás egyetlen utasításként önmagában áll. Nem része értékadásnak, vagy valamilyen kifejezésnek (pl összefűzés egy kiíratásban). Amikor azonban van visszatérési érték, azzal kezdeni kell valamit, különben nem lenne értelme az eredménynek. Eltároljuk egy változóban, közvetlenül kiírjuk a metódus eredményét a képernyőre, stb.

Paraméterként átadott tömbök, listák

A metódusoknak sokszor változókat adunk át paraméterként. A Java nyelvben ilyenkor a változók változók értéke kerül átadásra. Nem tudunk olyat megtenni, hogy az átadott változó értéke az eredeti helyen módosuljon.

Lássunk egy példát, miről is beszélek.

public static int duplaz( int szam )
{
  szam = szam * 2;
  return szam;
}

// metódus hívás valahol a main-ben:
public static void main( String[] args )
{
  int egesz = 4;
  int ketszeres = duplaz(egesz);
  System.out.println(egesz);
  System.out.println(ketszeres);
}
  • 1-5: A metódusunk csak annyit tesz, hogy kapott szám dupláját adja vissza.
  • 11: Ha a main-en belül egy számot eltárolok egy egesz nevű változóba, majd odaadom a metódusnak, az eredményt pedig eltárolom egy másik változóba,
  • 12: akkor az eredetileg átadott változó értéke nem változik meg,
  • 13: a metódus eredménye viszont az eredeti kétszerese lesz.

A tömbökkel és listákkal kicsit kacifántosabb a helyzet. Amikor létrehozzuk ezeket, a memóriában létrejön az adatszerkezet. Ha ezt hozzárendeljük egy változóhoz, akkor nem az elemeket tároljuk el benne, hanem azt a memóriacímet, ahol a tömb vagy lista elemei valójában megtalálhatóak. Ha ezt átadjuk  egy metódusnak, akkor valójában a változó értékeként a memóriacímet kapja meg! Miért gond ez?

Nézzük ezt a komplexebb példát. Kellene egy metódus, ami kiír egy kapott tömböt, és kell egy metódus, ami egy kapott tömböt növekvő sorrendbe rendez:

package tomb;

public class Tomb
{
  public static void kiir( int[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      System.out.print(tomb[i]+" ");
    }
    System.out.println();
  }
  
  public static int[] rendezNovekvo( int[] tomb )
  {
    int csere;
    for( int i = 0; i < tomb.length-1; i++ )
    {
      for( int j = i+1; j < tomb.length; j++ )
      {
        if( tomb[i] > tomb[j] )
        {
          csere = tomb[i];
          tomb[i] = tomb[j];
          tomb[j] = csere;
        }
      }
    }
    return tomb;
  }

  public static void main(String[] args)
  {
    int[] tomb = new int[20];
    for( int i = 0; i < tomb.length; i++ )
    {
      tomb[i] = (int)(Math.random()*41)-20;
    }
    
    System.out.println("Eredeti tomb: ");
    kiir(tomb);
    System.out.println("Rendezett tomb: ");
    tomb = rendezNovekvo(tomb);
    kiir(tomb);
  }
}

Elöl ott található kiemelve a kiírást és rendezést megvalósító metódus. Lássuk mi van a main-ben:

  • Ott a tömböm, 20 elemű, egészeket tartalmaz majd.
  • Feltöltöttem.
  • Kiírtam az eredeti értékeit.
  • Odaadtam a rendező metódusnak, aki rendezte, és az eredetit kicseréltem a rendezett tömbre.
  • Kiírtam a rendezett tömböt.

Nincs is ezzel semmi gond, pontosan ezt vártuk. Próbáld ki úgy, hogy a kiemelt 43. sort cseréld ki erre:

System.out.println("Eredeti tomb: ");
kiir(tomb);
System.out.println("Rendezett tomb: ");
rendezNovekvo(tomb);
kiir(tomb);

Futtasd le újra! Kopp. Mi van?

  1. Odaadtam a tömböt, hogy rendezze.
  2. A metódus rendezi a kapott tömböt, és visszaadja a rendezettet.
  3. A rendezett tömböt, amit visszakaptam, nem tároltam el, csak lóg a levegőben, nem használja senki. Ilyenkor a Java szemétgyűjtő algoritmusa egy idő után ki is takarítja a memóriából.
  4. A kiíratás alapján mégis megváltozott az eredeti tömb úgy, hogy annak új értéket nem adtam.

Összefoglalva: Adott egy rendezést végző metódus, ami egy rendezett tömböt ad eredményül. A metódust meghívom, odaadok neki egy tömböt, hogy rendezze, de az eredményt figyelmen kívül hagyom. Az eredeti tömb mégis rendezett lett!

És akkor mi van? Úgyis az volt a cél, hogy rendezd a tömböt. Na de mi a helyzet akkor, ha szeretnéd megtartani az eredeti tömböt is, és a rendezettet egy másik tömbbe szeretnéd tenni? Akkor alapból így próbálkoznál:

System.out.println("Eredeti tomb: ");
kiir(tomb);
System.out.println("Rendezett tomb: ");
int[] tombRendezett = rendezNovekvo(tomb);
kiir(tomb);

A metódus által rendezett tömböt egy másik tömbbe akarom tenni, hogy megmaradjon az eredeti is, és legyen egy rendezett tömböm is. Próbáld ki. A két tömb egyforma lesz! Ha egy metódusnak odaadok egy referencia típusú változót (tömb, lista, objektum), akkor amennyiben a metódus a kapott adatban bármit megváltoztat, akkor az az eredeti, hívó helyen lévő adat is megváltozik! Akkor is, ha az eredményt egy másik változóba tennéd!

Ez a probléma nem csak metódus használatakor jelentkezik. Próbáld csak ki ezt:

int[] t1 = new int[] { 1,2 };
int[] t2 = t1;
System.out.println("t1: "+t1[0]+" "+t1[1]);
System.out.println("t2: "+t2[0]+" "+t2[1]);

t1[0] = 100;
    
System.out.println("t1: "+t1[0]+" "+t1[1]);
System.out.println("t2: "+t2[0]+" "+t2[1]);

A gond a kiemelt sorban van. Ha egy tömböt egyenlővé teszek egy másikkal, akkor a két tömbváltozóban ugyanaz a memóriacím lesz, vagyis mindkettő ugyanarra az egyetlen létező adatszerkezetre (a tömb elemeire) mutat, vagyis bármelyiken változtatok, mindkettő megváltozik.

El lehet kerülni egyszerűen ezt a problémát? Szerencsére igen, mivel a Java készítői gondoltak ránk. A tömbnek (és listának is) létezik egy beépített metódusa, a clone(). Ez pontosan azt teszi, amire a neve utal, készít egy pontos másolatot az eredetiről. Mindezt úgy teszi, hogy valóban létrejön a memóriában egy, az eredetivel méret és értékek szerint megegyező másolat, de új memóriacímmel. Innentől a kettő egymástól független, bármelyikkel tehetek bármit, amit akarok, anélkül, hogy a másikat elrontanám.

Hogy valósíthatom ezt meg? Még mindig a tömb rendezéses példánál maradva, ha az eredményt az eredetitől függetlenül külön szeretném tárolni, akkor formailag ennyit kell tennem:

System.out.println("Eredeti tomb: ");
kiir(tomb);
System.out.println("Rendezett tomb: ");
int[] tombRendezett = rendezNovekvo(tomb.clone());
kiir(tomb);

Az eredeti tömb másolatát adom oda a rendező metódusnak, és az eredményt eltárolom a rendezett tömböt tartalmazó változómba. Természetesen ezt a két tömbös példában is használhatom:

int[] t1 = new int[] { 1,2 };
int[] t2 = t1.clone();
System.out.println("t1: "+t1[0]+" "+t1[1]);
System.out.println("t2: "+t2[0]+" "+t2[1]);

t1[0] = 100;

System.out.println("t1: "+t1[0]+" "+t1[1]);
System.out.println("t2: "+t2[0]+" "+t2[1]);

A clone() nem univerzális megoldás, csak egyetlen oldalról kezeli a problémát!

Egészen addig nincs gond, amíg a tömb vagy lista, amit a metódusnak odaadunk csak primitív elemeket tartalmaz. Oké, listának primitív elemeket nem adhatunk, ha mindenképpen egész számokat szeretnénk tárolni benne, akkor egy csomagoló osztályra (Integer) van szükségünk. Bár itt még bizonyos autoboxing is szerepet kap.

De mi van akkor, ha a tömb vagy lista nem primitív értékeket tartalmaz, hanem objektumokat? Később javarészt úgyis ezekkel dolgozunk majd. A helyzet az, hogy a clone() csak egy bizonyos mélységig dolgozik. Gyakorlatban ez azt jelenti, hogy létrehoz egy új tömböt vagy listát, ami független az eredetitől, de pontosan ugyanazok az objektumok lesznek benne (mivel az objektumok is csak mutatóként léteznek a változókban, hiszen referencia típusok). A két tömbben vagy listában az elemek sorrendjén változtathatok, ezek függetlenek lesznek. De amint valamelyik objektumon változtatok, akkor az mindkettőben változik!

Egy egyszerű példa erre a problémára tömb esetén. Adott egy egyszerű Kutya objektum. A kutyának csak neve van. Létrehozok egy 3 kutyából álló tömböt, majd készítek róla egy másolatot. Megcserélek két kutyát, majd valamelyiknek megváltoztatom a nevét.

package kutyatombreferencia;

class Kutya
{
  String nev;
  
  public Kutya( String nev )
  {
    this.nev = nev;
  }
}

public class KutyaTombReferencia
{
  public static void kiir( Kutya[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      System.out.print(tomb[i].nev+" ");
    }
    System.out.println();
  }

  public static void main(String[] args)
  {
    Kutya[] kutyak = new Kutya[3];
    
    kutyak[0] = new Kutya("Zulu");
    kutyak[1] = new Kutya("Sziszi");
    kutyak[2] = new Kutya("Nokedli");
    
    Kutya[] masolat = kutyak.clone();
    System.out.print("Eredeti tomb: ");
    kiir(kutyak);
    System.out.print("Masolat tomb: ");
    kiir(masolat);
    System.out.println();
    
    System.out.println("Kutya csere az eredeti tombben:");
    Kutya csere;
    csere = kutyak[0];
    kutyak[0] = kutyak[2];
    kutyak[2] = csere;
    
    System.out.print("Eredeti tomb: ");
    kiir(kutyak);
    System.out.print("Masolat tomb: ");
    kiir(masolat);
    
    System.out.println();

    System.out.println("Nev csere a masolat tombben:");
    masolat[1].nev = "Tunemeny";
    
    System.out.print("Eredeti tomb: ");
    kiir(kutyak);
    System.out.print("Masolat tomb: ");
    kiir(masolat);
  }
}

Ha kipróbálod a fenti kódot, akkor a következők szűrhetők le. A két tömb a clone() metódus miatt valóban független lesz egymástól, ha valamelyikben változtatok az elemek sorrendjén, akkor valóban csak abban a tömbben változik meg. Ha azonban bármelyik tömbben valamelyik elemet változtatom meg, akkor az mindkét tömbben megváltozik.

A clone() tehát csak a legfelső szinten, a tömböknél végez olyan másolást, ami egy új referenciával rendelkező, eredetitől különböző tömböt hoz létre, de ugyanazokat az elemeket helyezi el benne! Ezt figyelembe kell venni akkor is, ha ezek a tömbök egy metódus híváskor használatosak. A metódus sorrendet változtathat, de ha elemet változtat, akkor a metódus hatása visszahat az eredeti, híváskor átadott változó tartalmára is!

Összességében rögzítsük a következőket:

Minden primitív változó (egész, valós, char és boolean) minden esetben érték szerint kerül átadásra metódus híváskor. Minden egyéb adatszerkezet (tömb, lista, objektumok, stb) pedig referenciaként, vagyis a változóban tárolt címként kerül átadásra. Ezeknél az adatszerkezeteknél, ha a metódus módosítja a kapott referencia típusú adatszerkezetet, akkor annak tartalma a metódust meghívó helyen is megváltozik!

Komplex példa

Lássunk akkor egy komplexebb feladatot, ahol bizonyos feladatrészeket metódusba fogok kiemelni. Nem feltétlenül azzal a céllal teszem, hogy az ismétlődő tevékenységeket ne kelljen többször megírnom. Néha csak azért emelem ki, hogy lásd, hogyan is kell metódust készíteni.

package metodusok;

public class Metodusok {
  
  public static int[] feltolt20_20( int[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      tomb[i] = (int)(Math.random()*41)-20;
    }
    return tomb;
  }
  
  public static void kiir( int[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      System.out.print(tomb[i]+" ");
    }
    System.out.println();
  }
  
  public static void kiirParos12( int[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      if( tomb[i] % 2 == 0 && tomb[i] < 12 )
      {
        System.out.print(tomb[i]+" ");
      }
    }
    System.out.println();
  }
  
  public static boolean vanE_17( int[] tomb )
  {
    int hely = 0;
    while( hely < tomb.length && tomb[hely] >= -17 )
    {
      hely++;
    }
    
    if( hely < tomb.length )
    {
      return true;
    }
    else
    {
      return false;
    }
  }
  
  public static int osszegTomb( int[] tomb )
  {
    int osszeg = 0;
    for( int i = 0; i < tomb.length; i++ )
    {
      osszeg += tomb[i];
    }
    return osszeg;
  }
  
  public static int[] feltolt15_15( int[] tomb )
  {
    for( int i = 0; i < tomb.length; i++ )
    {
      tomb[i] = (int)(Math.random()*31)-15;
    }
    return tomb;
  }
  
  public static int maxTomb( int[] tomb )
  {
    int max = 0;
    for( int i = 1; i < tomb.length; i++ )
    {
      if( tomb[i] > tomb[max] )
      {
        max = i;
      }
    }
    return tomb[max];
  }

  public static int legkozelebb0( int[] tomb )
  {
    int min = 0;
    for( int i = 1; i < tomb.length; i++ )
    {
      if( Math.abs(tomb[i]) < Math.abs(tomb[min]) )
      {
        min = i;
      }
    }
    return tomb[min];
  }

  public static int minTomb( int[] tomb )
  {
    int min = 0;
    for( int i = 1; i < tomb.length; i++ )
    {
      if( tomb[i] < tomb[min] )
      {
        min = i;
      }
    }
    return tomb[min];
  }
  
  public static int[] kivalogatParos( int[] tomb )
  {
    int db = 0;
    for( int i = 0; i < tomb.length; i++ )
    {
      if( tomb[i] % 2 == 0 )
      {
        db++;
      }
    }
    
    if( db == 0 )
    {
      return new int[0];
    }
    
    else
    {
      int[] tomb2 = new int[db];

      db = 0;
      for (int i = 0; i < tomb.length; i++)
      {
        if (tomb[i] % 2 == 0)
        {
          tomb2[db] = tomb[i];
          db++;
        }
      }
      return tomb2;
    }
  }
  
  public static int[] rendezNovekvo( int[] tomb )
  {
    int csere;
    for( int i = 0; i < tomb.length-1; i++ )
    {
      for( int j = i+1; j < tomb.length; j++ )
      {
        if( tomb[i] > tomb[j] )
        {
          csere = tomb[i];
          tomb[i] = tomb[j];
          tomb[j] = csere;
        }
      }
    }
    return tomb;
  }
  
  public static int[] rendezCsokkeno( int[] tomb )
  {
    int csere;
    for( int i = 0; i < tomb.length-1; i++ )
    {
      for( int j = i+1; j < tomb.length; j++ )
      {
        if( tomb[i] < tomb[j] )
        {
          csere = tomb[i];
          tomb[i] = tomb[j];
          tomb[j] = csere;
        }
      }
    }
    return tomb;
  }

  public static double atlag( int[] tomb )
  {
    return (double)osszegTomb(tomb) / tomb.length;
  }

  public static void main(String[] args)
  {
    // 1. feladat
    // Tölts fel egy 10 elemű tömböt a [-20;20] intervallumból,
    // és írd ki egymás mellé az elemeket,
    // szóközzel elválasztva.
    System.out.println("\n1. feladat");
    int[] tomb = new int[10];
    tomb = feltolt20_20(tomb);
    kiir(tomb);
    
    // 2. feladat
    // Írd ki azokat a páros elemeket,
    // amelyek nem érik el a 12-őt.
    System.out.println("\n2. feladat");
    System.out.print("A tomb azon paros elemei, amelyek nem "+
             "erik el a 12-ot: ");
    kiirParos12(tomb);
    
    // 3. feladat
    // Van-e a tömbben -17-nél kisebb elem?
    System.out.println("\n3. feladat");
    
    if( vanE_17(tomb) )
    {
      System.out.println("Van -17-nel kisebb eleme.");
    }
    else
    {
      System.out.println("Nincs -17-nel kisebb eleme.");
    }
    
    // 4. feladat
    // Írd ki a tömb elemeinek összegét.
    System.out.println("\n4. feladat");
    
    System.out.println("A tomb elemeinek osszege: "+
               osszegTomb(tomb));

    // 5. feladat
    // Töltsd fel újra a tömböt a [-15;15] intervallumból, 
    // és írd ki egymás mellé az elemeket,
    // szóközzel elválasztva.
    System.out.println("\n5. feladat");
    System.out.print("A tomb elemei a [-15;15] "+
             "intervallumbol sorsolva: ");
    tomb = feltolt15_15(tomb);
    kiir(tomb);
    
    // 6. feladat
    // Melyik a tömb legnagyobb eleme?
    System.out.println("\n6. feladat");
    System.out.print("A tomb legnagyobb eleme: ");
    System.out.println( maxTomb(tomb) );
    
    // 7. feladat
    // Melyik elem van a legközelebb a nullához?
    System.out.println("\n7. feladat");
    System.out.println("A "+legkozelebb0(tomb)+" elem van "+
               "legkozelebb a nullahoz.");
    
    // 8. feladat
    // Hány maximuma van a tömbnek?
    System.out.println("\n8. feladat");
    int maxErtek = maxTomb(tomb);
    int db = 0;
    for( int i = 0; i < tomb.length; i++ )
    {
      if( tomb[i] == maxErtek )
      {
        db++;
      }
    }
    System.out.println(db+" maximuma van a tombnek.");

    // 9. feladat
    // Írd ki, melyik az a legkisebb intervallum, amelybe a
    // tömbben lévő elemek beleférnek! 
    System.out.println("\n9. feladat");
    System.out.println("A legkisebb intervallum, amelyben a "+
               "tomb elemei benne vannak: ");
    System.out.println("["+minTomb(tomb)+";"+maxTomb(tomb)+"]");
    
    // 10. feladat
    // Rendezd a tömb elemeit növekvő sorrendbe,
    // és írd ki egymás mellé az elemeket,
    // szóközzel elválasztva!
    System.out.println("\n10. feladat");
    int[] rendezettTomb = rendezNovekvo(tomb.clone());
    System.out.println("A tomb elemei novekvo sorrendben: ");
    kiir(rendezettTomb);
    
    // 11. feladat
    // Válogasd ki a tömb páros elemeit a tömbből
    // egy másik tömbbe!
    System.out.println("\n11. feladat");
    int[] parosTomb = kivalogatParos(tomb);
    System.out.print("A kivalogatott paros szamok: ");
    kiir( kivalogatParos(tomb) );

    // 12. feladat
    // A kiválogatott tömbben melyik a második legnagyobb elem?
    // (az egyforma számokat is különbözőnek vesszük)
    System.out.println("\n12. feladat");
    System.out.println("A kivalogatott tomb masodik "+
               "legnagyobb eleme: ");
    int[] csokkenoParosTomb = rendezCsokkeno(parosTomb.clone());
    
    if( csokkenoParosTomb.length < 2 )
    {
      System.out.println("Nincs ket elem a kivalogatott "+
                 "tombben.");
    }
    else
    {
      System.out.println("A kivalogatott tomb 2. "+
                 "legnagyobb eleme: "+csokkenoParosTomb[1]);
    }
    
    // 13. feladat
    // A kiválogatott tömbben melyik a második legnagyobb szám?
    // (az egyforma számokat is azonosnak vesszük)
    System.out.println("\n13. feladat");
    System.out.println("A kivalogatott tomb masodik "+
               "legnagyobb szama: ");
    if( csokkenoParosTomb.length < 2 )
    {
      System.out.println("Nincs ket elem a kivalogatott "+
                 "tombben.");
    }
    
    boolean masodikMax = false;
    for( int i = 1; i < csokkenoParosTomb.length; i++ )
    {
      if( csokkenoParosTomb[i] != csokkenoParosTomb[0] )
      {
        System.out.println("A kivalogatott tomb 2. "+
                   "legnagyobb szama: "+csokkenoParosTomb[i]);
        masodikMax = true;
        break;
      }
    }
    
    if( !masodikMax )
    {
      System.out.println("Nincs masodik maximum a "+
                 "kivalogatott tombben.");
    }
    
    // 14. feladat
    // Írd ki a tömb elemeinek átlagát
    System.out.println("\n14. feladat");
    System.out.println("A tomb elemeinek atlaga: "+
               atlag(tomb) );
  }
}

Lássunk akkor néhány kiemelt dolgot:

  • 43-50 – Ezt a részt akár ezzel is helyettesíthettem volna:
    return hely < tomb.length;
  • 182 – Ha megnézed, az átlag számító metódus részfeladatként felhasználja az összeg számító metódust, akinek odaadja azt a tömböt, amit ő átlagolni szeretne. Az összeg számító megadja a tömb elemeinek összegét, amit csak osztani kell a tömb méretével. A két metódus tetszőleges sorrendben szerepelhet egymáshoz képest, nincs olyan probléma, mint C++ esetén, hogy egy metódus nem látja a mögötte lévőt, hacsak nem írom ki a kód elejére a hátsó metódus szignatúráját. Lásd: C++ saját függvgények.
  • 208 – Ebben a sorban nem azt vizsgálom, hogy
    if( vanE_17(tomb) == true )

    helyette ez a vizsgálat is elég:

    if( vanE_17(tomb) )

    A vanE_17 metódus önmagában logikai típusú eredményt ad, amit felesleges egy logikai értékhez hasonlítani egyenlőség vizsgálattal.

  • 243 – A metódus eredményét akár bele is fűzhetem a válaszba.
  • 266 – Az előzőhöz hasonlóan akár több metódus eredményét is beilleszthetem közvetlenül a válaszba.
  • 273, 291 – Új rendezett tömböt akarok létrehozni. Az eredeti tömb másolatát adom oda, hogy a rendezés ne hasson az eredeti tömbre.

Mivel a cikk írásakor már a 88. változatnál járok, egyelőre munkapéldányként teszem közzé ezt a leckét. Még esetleg a return témakört kicsit körbejárom, de most elfogyott a türelmem. Pár héten belül úgyis kiderül, milyen hibákat találtok benne. Ezeket majd javítom, és akkor nyilvánítom véglegesnek. Addig maradjon ez a példány nyilvános. A leckék sorrendjét is át szeretném majd dolgozni, tehát a sorszámok is változni fognak. Egyelőre ez marad a 22-es, ami úgyis hiányzik a sorszámozásból 🙂 Addig is használjátok egészséggel!

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez az oldal az Akismet szolgáltatást használja a spam csökkentésére. Ismerje meg a hozzászólás adatainak feldolgozását .