Gyakorlati alapok
A programozók rémálma: a kódugrás (a címkék /label/ használata)
Címke (label) + break
Az előző fejezet gondolatait továbbvezetve, amely problémára a Java alkotói felfigyeltek, hogy adott például egy többszörösen egymásba ágyazott for ciklus, általánosságban egy olyan kódszerkezet, amelyik főciklust-főblokkot és benne további alciklusokat-alblokkokat tartalmaz. (Ágyazzunk be a for ciklusnak! (az egybeágyazott for ciklusok) című fejezet) Az alábbi futtatható Java-kód 2 db egymásba ágyazott (nested) for ciklust mutat:
public class Main {
public static void main(String[] args) {
int []tomb1 = new int []{1, 2, 3, 4, 5};
int []tomb2 = new int []{5, 4, 3, 2, 1};
for (int i = 0; i < tomb1.length; i++) {
for (int j = 4; j >= 0; j--) {
}
}
}
}
A fenti kód hibátlanul lefut, de lényegében semmit nem tesz a 2 db létrehozott tömbbel.
Nos, mi történik abban az esetben, ha a belső alciklusban vizsgálunk meg egy vagy több feltételt és teljesülésük esetén már nincs szükségünk a ciklusok további futtatására? Ekkor azt tapasztaljuk, hogy a főciklus fut tovább, sokszor viszont ezt nem kívánnánk és szeretnénk belőle is kilépni.
Adott például 2 db int típusú tömb...
int []tomb1 = new int []{1, 2, 3, 4, 5};
int []tomb2 = new int []{5, 4, 3, 2, 1};
...amelyek elemei növekvő és csökkenő egész számok. A 2 for ciklus a 2 tömb egyik elemét (int 3) keresi, a főciklus növekvő, a belső alciklus csökkenő lépésben. Ha valahol találkoznak, amelyet egy flag, azaz állapotváltozó tárol (boolean talalkoztak = true;), akkor a belső ciklus futása meg fog szakadni (break), ám a főciklus fut tovább, emiatt az üzenet sokszor le fog futni: "Ez sokszor le fog futni".
public class Main {
public static void main(String[] args) {
int []tomb1 = new int []{1, 2, 3, 4, 5};
int []tomb2 = new int []{5, 4, 3, 2, 1};
boolean talalkoztak = false;
System.out.println("Ciklusok előtt.");
for (int i = 0; i < tomb1.length; i++) {
for (int j = 4; j >= 0; j--) {
if (tomb1[i]
== tomb2[j]){
talalkoztak = true;
System.out.println(talalkoztak);
break;
}
}
System.out.println("Ez sokszor le fog
lefutni.");
}
System.out.println("Kilépve a külső for ciklusból.");
}
}
Végeredmény:
Ciklusok előtt.
true
Ez sokszor le fog lefutni.
true
Ez sokszor le fog lefutni.
true
Ez sokszor le fog lefutni.
true
Ez sokszor le fog lefutni.
true
Ez sokszor le fog lefutni.
Kilépve a külső for ciklusból.
Az outer címke használatával a belső ciklus futása is megszakítható. A címkét közvetlenül a főciklus fölé kell helyeznünk, utána már más utasítás nem jöhet. A ciklusmegszakítást a belső ciklus feltétele és egyéb utasítások alá kell deklarálnunk:
if (tomb1[i] == tomb2[j]){
talalkoztak = true;
System.out.println(talalkoztak);
break outer;
A feltétel teljesülése esetén a belső ciklus futása megszakad, a program a főciklusból is kilép, az "Ez nem fog lefutni." üzenet soha nem jelenik meg, a vezérlés a főciklus utánra kerül, emiatt pedig lefut az utolsó üzenet: "Outer címke után, kilépve a külső for ciklusból.".
public class Main {
public static void main(String[] args) {
int []tomb1 = new int []{1, 2, 3, 4, 5};
int []tomb2 = new int []{5, 4, 3, 2, 1};
boolean talalkoztak = false;
System.out.println("Outer címke előtt.");
outer:
for (int i = 0; i < tomb1.length; i++) {
for (int j = 4; j >= 0; j--) {
if (tomb1[i] == tomb2[j]){
talalkoztak = true;
System.out.println(talalkoztak);
break outer;
}
}
System.out.println("Ez nem fog lefutni.");
}
System.out.println("Outer címke után, kilépve a külső for ciklusból.");
}
}
A példából jól láthatjuk, hogy mennyire szabályozottan és korlátozottan használható fel a goto jellegű, valójában kódugrálásos utasítás: lényegében csakis a ciklusok közvetlen közelében.
További trükk, hogy a címkézés valójában bármilyen, programozás-technikailag ésszerű szó esetén működőképes.
Az alábbi, futtatható Java-kódban az outer címkét barmiCimke-re cseréltük:
public class Main {
public static void main(String[] args) {
int []tomb1 = new int []{1, 2, 3, 4, 5};
int []tomb2 = new int []{5, 4, 3, 2, 1};
boolean talalkoztak = false;
System.out.println("barmiCimke előtt.");
barmiCimke:
for (int i = 0; i < tomb1.length; i++) {
for (int j = 4; j >= 0; j--) {
if (tomb1[i] == tomb2[j]){
talalkoztak = true;
System.out.println(talalkoztak);
break barmiCimke;
}
}
System.out.println("Ez nem fog lefutni.");
}
System.out.println("barmiCimke után, kilépve a külső for ciklusból.");
}
}
A felfedezéseken továbblépve észrevehetjük, hogy nemcsak a főciklus, hanem bármelyik alciklus felcímkézhető, feltéve, hogy annak van valamiféle programozás-technikai értelme.
Az alábbi futtatható Java-kód a kiindulópont: benne 3 db egymásba ágyazott for ciklus pörög teljes erővel. Még nincsenek felcímkézve, először inkább értsük meg működését:
-
a külső ciklus értéke i léptetője 1 és 2 lehet, ez ki is van írva a konzolra,
-
a középső ciklus értéke j léptetője 3 és 4 lehet, ez ki is van írva a konzolra,
-
a külső ciklus értéke k léptetője 5 és 6 lehet, ez ki is van írva a konzolra.
Az olvashatóság érdekében kis formázást is elkövettem kódkiíráskor a System.out.println() használatával.
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 2; i++){
System.out.println();
System.out.print(i + " ");
for (int j =
3; j <= 4; j++){
System.out.println();
System.out.print(j + " ");
for(int k = 5; k <= 6; k++){
System.out.print(k + " ");
}
}
}
}
}
Végeredmény:
1
3 5 6
4 5 6
2
3 5 6
4 5 6
A ciklusok lefutása a végeredményből leolvasva egyértelmű.
Most felcímkézzük a középső ciklust (belso:) és a belső ciklusba tesszük a feltételt, valamint a kód töréspontját:
if(k == 5)
break belso;
public class
Main {
public static void main(String[] args) {
System.out.println("Címkehasználat előtt.");
for (int i = 1; i <= 2; i++){
System.out.println();
System.out.print(i + " ");
belso:
for (int j =
3; j <= 4; j++){
System.out.println();
System.out.print(j + " ");
for(int k = 5; k <= 6; k++){
System.out.print(k + " ");
if(k == 5)
break belso;
}
}
}
System.out.println("\nCímkehasználat után.");
}
}
Végeredmény:
Címkehasználat előtt.
1
3 5
2
3 5
Címkehasználat után.
A kód az utasításnak megfelelően megszakad k = 5 után (5 még ki lesz írva, 6 már nem). Mivel csakis a középső ciklus futása van megszakítva, ezért a főciklus újra fog indulni (2 3 5). A középső ciklus újfent meg van szakítva, ekkor azonban már kilépünk a főciklusból is.
A következő kódban felcímkézzük a főciklust (kulso:), de ez valójában még nincs hatással semmire, hiszen nem kötöttünk hozzá semmilyen további utasítást:
public class
Main {
public static void main(String[] args) {
System.out.println("Címkehasználat előtt.");
kulso:
for (int i = 1; i <= 2; i++){
System.out.println();
System.out.print(i + " ");
belso:
for (int j =
3; j <= 4; j++){
System.out.println();
System.out.print(j + " ");
for(int k = 5; k <= 6; k++){
System.out.print(k + " ");
if(k == 5)
break belso;
}
}
}
System.out.println("\nCímkehasználat után.");
}
}
Végeredmény:
Címkehasználat előtt.
1
3 5
2
3 5
Címkehasználat után.
Az alábbi, futtatható Java-kódban már 2 töréspontot iktattunk be, a belso maradt a helyén, azonban a középső ciklusba egy i == 2 feltételt és további töréspontot tettünk (break kulso:). A főciklust is a kulso felcímkézve végül teljesen kilépünk ebből a burjánzó cikluscsokorból.
public class
Main {
public static void main(String[] args) {
System.out.println("Címkehasználat előtt.");
kulso:
for (int i = 1; i <= 2; i++){
System.out.println();
System.out.print(i + " ");
belso:
for (int j = 3; j <= 4; j++){
System.out.println();
System.out.print(j + " ");
if(i == 2){
break kulso;
}
for(int k =
5; k <= 6; k++){
System.out.print(k + " ");
if(k == 5)
break belso;
}
}
}
System.out.println("\nCímkehasználat után.");
}
}
Végeredmény:
Címkehasználat előtt.
1
3 5
2
3
Láthatjuk, hogy még ebben a programozás-technikailag igen szabályozott környezetben is eléggé nehéz a kód megértése, visszafejtése, ráadásul szinte egyszerre 2 címkével dolgoztunk. Főleg emiatt a programozók nem is nagyon szeretik a címkéket, sőt néhányan -egyértelmű előnyük ellenére-, óva intenek használatuktól.
Fontos még megjegyeznünk, hogy címkéket nemcsak egybeágyazott for ciklusok, hanem bármilyen belső jellegű (például do - while) ciklus esetén használhatunk.
Ennek bizonyítására alkossunk egy összegubancolódott, címkés cikluspárost egy for ciklus és egy do - while ciklus használatával:
public class
Main {
public static void main(String[] args) {
int j = 5;
System.out.println("Outer címke előtt.");
outer:
for(int i = 0; i <= 5; i++) {
do{
System.out.println("Még nem találkoztak: " + i + " != " + j);
j--;
if(i == j){
System.out.print("Most találkoztak: " + i + " = " + j);
break outer;
}
}while(j == 0);
System.out.println("i értéke: " + i);
}
System.out.println("\nOuter címke után, kilépve a külső for
ciklusból.");
}
}
Végeredmény:
Outer címke előtt.
Még nem találkoztak: 0 != 5
i értéke: 0
Még nem találkoztak: 1 != 4
i értéke: 1
Még nem találkoztak: 2 != 3
Most találkoztak: 2 = 2
Outer címke után, kilépve a külső for ciklusból.
Az algoritmus 3 működési szintű:
-
szint - for ciklus
-
szint - do-while ciklus
-
szint - if feltétel
A címke a 3. legbelső szinten található. A kód kissé erőltetett, amennyiben a problémát szerintem meg lehetett volna oldani egyszerűbben is, de most a cél különböző jellegű ciklusok ütköztetése volt. Az algoritmus valóban outer címkére ugrik ki, mert i értéke a feltétel teljesülése után a főciklusban már nem íródik ki.
Összegezve: személyes tapasztalatom az, hogy előfordulhat olyan programozási probléma, amelynek eklatáns vagy éppen optimális megoldása éppen 1 vagy 2 jól elhelyezett címke. Hétköznapi programozási környezetben csakis nagy gonddal és figyelemmel használjuk, de ha van rá lehetőség, inkább hanyagoljuk.