Java SE 8 Documentation
Feladatok

Kivételek és kezelésük

A kivétel egy olyan (többnyire hiba által keltett) esemény, amely váratlan bekövetkezésével megszakítja a programunk tervezett menetét. Ha fel akarunk készülni minden ilyen lehetőségre, akkor ahhoz a forrást tele kell tűzdelnünk folyamatos hibaellenőrzéssel. Ettől viszont tulajdonképpen olvashatatlanná válik az egészt. A kivételkezelés ezt igyekszik orvosolni, egy olyan vezérlési szerkezet bevezetésével, amely lehetővé teszi, hogy az algoritmust egy idealizált módon írjuk le, és a hibák kezelését külön, egy összevont helyen adjuk meg.

A kivételek kezelésének sematikus módja Javaban:

int i, j;

// kritikus utasítások egységbezárása
try {
    // kritikus utasítások
    i = Integer.parseInt(args[0]);
}
// (kivétel)specifikus kivételkezelő ágak (opcionális)
// az első ág, amelybe a kivétel az osztályhierarchia (alaposztály:
// Throwable) szerint beillik, lekezeli a kivételt;
// a kivétel újbóli kiváltása lehetséges
catch (NumberFormatException e1) {
    // kivételkezelő: ne legyen üres!
    System.err.println("Ooops, this was a number format exception.");
    System.err.println(e.getMessage());  // hibaüzenet a szabványos hibakimenetre
    e.printStackTrace();                 // stack trace
    i = 0;
}
// további ágak
catch (Exception e2) {
    // egyéb kivételekhez tartozó kivételkezelő
    System.err.println("Hrm, some other error happened.");
    System.err.println(e.getMessage());
}
// A kritikus blokk után minden esetben lefutó utasítások (opcionális)
finally {
    j = 42;
}

Tehát lényegében azokat az utasításokat, amelyek működése közben bármilyen zavar keletkezhet a futásban, egy közös programblokkba tesszük. Ha ezen a blokkon belül történik valami váratlan (vagyis egy kivétel váltódik ki), akkor ennek a blokknak a futása megszakad és a vezérlés a kivételkezelőnek adódik át. Ennek a feladata onnantól, hogy gondoskodjon ilyen váratlan események esetén a program kulturált működéséről, például jelezze a felhasználónak a hiba okát és szabályosan állítsa le magát. Ezt a blokkot a try — vagyis "megpróbálni" — kulcsszó vezeti be, utalva arra, hogy a futása bármikor megszakadhat.

A váratlan eseményeket a Java nyelvben a java.lang.Throwable osztály írja le, amely azok általános modellje. Ennek specializációjaként, vagyis a nyelvben származtatott osztályaiként definiálható az összes többi ilyen esemény. A Throwable — "eldobható" — név abból származik, hogy ezeket a Java terminológia szerint nem kiváltani, hanem eldobni lehet. Ezt a throw — "dobni" — utasítás valósítja meg, amelynek paramétere a Throwable vagy annak valamelyik leszármazottjának egy objektumpéldánya. (Tehát a váratlan események a rendszerben objektumokkal ábrázoljuk lényegében.)

throw new Error("Error!");

És ha a kivételeket dobjuk, akkor a lekezelést "elkapásnak" nevezik, amelyet a catch kulcsszó vezet be. A catch a try után közvetlnül következik, és segítségével azt lehet leírni, hogy melyik Throwable leszármazottat milyen módon akarunk feldolgozni. Az osztályok — mivel köztük ős-leszármazott viszony van — ilyenkor a specializáltabbaktól az általánosabbakig haladva kell, szerepeljenek, ha belőlük több fajtát is le akarunk kezelni egyszerre.

A kivételek típusai

  • Felügyelt kivételek, java.lang.Exception: definiálni kell ezeket a metódus fejlécében a throws kulcsszó megadása után. Ha definiáltak, le is kell kezelni. Ellenkező esetben fordítási hibát fogunk kapni.

    static boolean isErrorProne(int x) throws Exception {
        if (x > 42) throw new Exception("Yes, that is an error.");
        return false;
    }
  • Nem felügyelt kivételek: nem kötelező sem definiálni, sem lekezelni, ilyenek például a java.lang.NullPointerException, java.lang.ArrayIndexOutOfBoundsException, java.lang.NumberFormatException kivételosztályok. Ezeket alapvetően megfelelően megírt programok esetében nem is nagyon láthatjuk. Viszont hasznos, hogy vannak, mert így a hibák keresését megkönnyítik.

  • Kritikus esetekben fordulnak elő: java.lang.Error. Ezeket nem érdemes elkapni, mert ilyenek kiváltódásakor már nem sok esélyünk van a program helyes működésének visszaállítására egyébként sem.

A kivételek hierarchiája

A kivételek hierarchiája

A throw-catch páros látszólag kicsit hasonlít a return utasításhoz, mivel ahhoz hasonlóan megszakad a blokk és kilépünk belőle. Viszont ilyenkor nem feltétlenül maga a függvény fejeződik be, hanem az összes olyan azt befoglaló programegység befejeződik addig, amíg a kivételt le nem kezeljük. Ezért, ha a return helyett akarjuk használni, inkább ne tegyük!

Hézagok a kivételkezelésben

A kivételkezelők megadásakor a következőkre viszont mindig érdemes ügyelni:

  • lehetőség szerint fedjük le az összes váratlan eseményt. Ha nem tudjuk mindegyiket feltárni, akkor használjunk általánosabb változatokat, például a java.lang.Exception osztályt. Ha ugyanis ezt nem tesszük meg, akkor a váratlan esemény tovább fog gyűrűzni a programon és egy szinttel fentebb jelenik meg újra, egészen addig, amíg a teljes program le nem áll.

  • tartózkodjunk a bonyolult kivételkezelők írásától, mivel akár ott is bekövetkezhetnek váratlan események. Ekkor mondjuk azt, hogy a kivételkezelőben keletkezik kivétel.

      int x, y;
      try {
          x = Integer.parseInt(args[0]);
          y = Integer.parseInt(args[1]);
      }
      catch (NumberFormatException nfe) {
          System.err.println("Either \"" + args[0] + "\" or \"" + args[1] + "\" is invalid.");
      }

A kivételek alkalmazása: Input/Output

Váratlan események leginkább a bemenet vagy kimenet kezelésekor következhetnek be, ezért a kivételek egyik legnagyobb felhasználói az ilyen jellegű osztályok és metódusok. Ennek következtében, amikor állományokat kezelünk, nagyon könnyen futhatunk olyan metódusba, amely kötelezően lekezelendő kivételt vált ki.

import java.io.FileWriter;
import java.io.PrintWriter;

public class WriteSampleFile {
    public static void main(String[] args) throws Exception {
        PrintWriter pw = new PrintWriter(new FileWriter("dummy.txt"));
        pw.println("Enter your data here.");
        pw.close();
    }
}

Javaban a ki- és bementetet adatfolyamokba, streamekbe, szervezik, több különböző módon:

Ezeknek vannak további specializált változatai, a teljesség igénye nélkül:

Feladat szerinti csoportosítás:

Alapvető műveletek

Összefoglalásképpen felsoroljuk az állományok kezelését megvalósító osztályok jellemző műveleteit. Itt fontos tudni, hogy az állományokhoz, miután megnyitottuk ezeket, egy ún. állománymutató társul, amely mindig jelzi az írás/olvasás aktuális pozícióját. Lényegében úgy viselkedik, mint egy végtelenített lista, amelyen szekvenciálisan tudunk végighaladni.

A másik fontos, háttérben szereplő koncepció a puffer, amely egy olyan speciális memóriaterület, ahova az állomány egyes (beolvasott vagy kiírt) részei kerülnek és majd innen tovább a lemezre. Ennek szerepe a hatékonyság növelése, de ez olykor az elvárttól eltérő viselkedést is eredményezhet. Például amíg nem ürül a puffer, addig az állományban sem jelenik meg semmi. Ezt ki kell kényszeríteni: vagy további adat írásával, vagy pedig a megfelelő metódussal.

Tehát a műveletek:

  • Megnyitás: automatikus (többnyire a konstruktorban)
  • Lezárás: close()
  • Állománypuffer tartalmának kiírása: flush()
  • Adott mennyiségű bájtnyi adat kihagyása, "ugrás": skip()
  • Írás: write(), print()
  • Olvasás: read(), ha a csatornában nincs olvasandó adat, akkor az olvasási művelet várakozik annak megjelenésére (blokkol):

    public static void main(String[] args) throws Exception {
        int i = System.in.read();
        System.out.println("The character was just read: " + i);
    }
  • Könyvjelzők (ha támogatott): markSupported(), mark(), reset()
  • Csatorna ürességének ellenőrzése: ready()
  • Rendelkezésre álló olvasható bájtok számának lekérdezése: available()

    int size = new FileInputStream("dummy.txt").available();

Példák

Állomány írása:

import java.io.FileNotFoundException;
import java.io.PrintWriter;

public class WriteFile {
    public static void main(String[] args) {
        PrintWriter pw = null;

        try {
            pw = new PrintWriter(args[0]);
            pw.println("Line 1");
            pw.println("Line 2");
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        finally {
            if (pw != null)
                pw.close();
        }
    }
}

Állomány olvasása:

public class ReadFile {
    public class void main(String[] args) {
        BufferedReader br = null;

        try {
            br = new BufferedReader(new FileReader(args[0]));
            String line = null;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        finally {
            if (br != null) {
                try {
                    br.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Közvetlen elérés

A korábbiakkal ellentétben a java.io.RandomAccessFile már bájtok tömbjéhez hasonlóan olvasható és írható, tetszőleges sorrendben. Itt is egy mutató jelzi az aktuális pozíciót, amely lekérdezhető (getFilePointer()), állítható (seek()). java.io.DataInput és java.io.DataOutput osztályként is tud viselkedni, a műveleteivel tetszőleges típus írható, olvasható (úgy használható, mint a java.io.DataInputStream, java.io.DataOutputStream: write*(), read*() függvények), illetve a bájtok átugorhatóak (skip()).

import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomFileTest {
    public static void main(String[] args) throws IOException {
        RandomAccessFile raf = new RandomAccessFile("dummy.dat", "rw");

        raf.writeInt(0xCAFEBABE);
        raf.seek(16);
        raf.writeInt(0xDEADBEEF);
        raf.seek(32);
        raf.writeInt(0xBADF00D0);
        raf.seek(48);
        raf.writeInt(0xDEADC0DE);
        raf.close();
    }
}

Szöveges állományok olvasása a java.util.Scanner segítségével

Érdemes hozzátenni, hogy a korábban megismert java.util.Scanner osztály is alkalmas szöveges állományok feldolgozására. Ehhez mindössze a megfelelő konstruktorát kell használunk, amelynek a paramétere egy java.io.File típusú objektum. A java.io.File segítségével állományokat tudunk ábrázolni. Ha ennek konstruktorát hívjuk meg egy String értékkel, akkor azt elérési útvonalnak fogja tekinteni, és a java.util.Scanner számára egy megnyitandó, feldolgozandó állományt fog közvetíteni.

A korábbiakkal ellentétben itt arra kell ügyelni, hogy egyrészt az állomány feldolgozása során különféle kivételek merülhetnek fel, illetve amikor befejeztük a vele való munkát, akkor meg kell hívni a close() metódust. (Máskülönben a háttérben használt állomány nem fog lezáródni, és ezzel erőforrást fogunk pazarolni!)

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Scanner;

public class FileScanner {
    public static void main(String[] args) throws IOException {
        Scanner sc = null;

        try {
            sc = new Scanner(new File(args[0]));
            while (sc.hasNextLine()) {
                String line = sc.nextLine();
                System.out.println(line);
            }
        }
        catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        finally {
            if (sc != null) {
                sc.close();
            }
        }
    }
}

Számok véletlenszerű előállítása

Olykor hasznos lehet a programjainkban, hogy ne a felhasználótól kérjünk be információkat, és ne is mi magunk tegyük bele konstansként a programba ezeket, hanem bízzuk az egészet a véletlenre! A programokban lehetőségünk van ún. álvéletlenszámok generálására, amellyel bizonyos fokig tudjuk szimulálni a véletlent a programjainkban.

  • a java.util.Random osztály képes véletlenszerű egész számokat, lebegőpontos számokat, logikai értékekek stb. adott tartományban előállítani.

  • a java.lang.Math.random() statikus metódussal lehet olyan véletlenszerű lebegőpontos számokat létrehozni, amelyek nullától nagyobb vagy egyenlőek és kisebbek, mint egy (normált érték).

Ezek közül a java.util.Random osztály használata a javasolt. Továbbá érdemes megjegyezni, hogy elengendő ezt az osztály egyszer példányosítani, utána a megfelelő metódusokat meghívva folyamatosan véletlenszerű számokat kapunk. Lehetőségünk van viszont relatíve véletlenszerű számokat készíteni úgy, ha a java.util.Random példányosításakor a konstruktornak megadjuk az ún. seed vagy magértéket. Ekkor az így létrehozott objektumból kiolvasott véletlenszerű számok egymáshoz képest ugyan véletlen lesznek, viszont mindig ugyanazt a sorozatot kapjuk belőlük ugyanarra a seed értékre.

Például hozzunk létre a felhasználó által parancssorból megadott mennyiségű véletlenszerű számot egy szintén a felhasználó által megadott tartományban!

import java.util.Random;

public class RandomInteger {
  public static void main(String[] args){
    int n, lowerBound, upperBound;

    try {
        n          = Integer.parseInt(args[0]);
        lowerBound = Integer.parseInt(args[1]);
        upperBound = Integer.parseInt(args[2]);
    }
    catch (Exception e) {
        System.err.println("Not enough or invalid parameters were given.");
        System.exit(1);
    }

    System.out.printf("Generating %d random integers in range [%d..%d]...",
      n, lowerBound, upperBound);

    Random randomGenerator = new Random();
    int range = (upperBound - lowerBound);

    for (int i = 0; i < n; ++i){
        int r = lowerBound + randomGenerator.nextInt(range);
        System.out.println("Generated: " + r);
    }
  }
}

Feladatok

  • Készítsünk egy olyan programot, amely két parancssori argumentumot kap: egy beolvasandó állomány elérési útját és egy szöveget! A program kezdje el olvasni az állományt, majd:

    • tekintse a szöveget egyetlen szónak és számolja meg az előfordulásainak számát!

    • írja ki belőle a szabványos kimenetre azokat a sorokat, amelyekben a szöveg előfordul!

  • Valósítsunk meg egy olyan programot, amely paranccsori paraméterként megkapja egy kiírandó állomány elérési útját, majd:

    • a szabványos bemenetről beolvasott sorokat (egészen az üres sorig) rögzíti benne!

    • (parancssorban meg)adott mennyiségű véletlenszerű:

      • karakterrel feltölti!
      • bájttal feltölti!

    Minden esetben ellenőrizzük, hogy az állomány létezik-e már és ha igen, akkor írjuk felül!

  • Írjunk DateGenerator névvel egy olyan osztályt, amelyben csak egyetlen paraméter nélküli osztályszintű metódus érthető el kívülről generate() névvel. Ennek a metódusnak az a feladata, hogy minden meghíváskor véletlenszerűen létrehozzon egy érvényes Date típusú objektumot. A Date objektum dátumokat ábrázol, ahol írásvédetten eltároljuk az évet, hónapot és napot.

    • Legyen a generate() metódusnak egy olyan változata, amely adott számú Date objektumot hoz létre és azt egy tömbbel adja vissza!

    • Egészítsük ki a Date osztályt egy olyan metódussal, amely bájtok tömbjévé tudja alakítani annak belső állapotát!

    • Az előbbi metódust felhasználva valósítsuk meg a generate() egy olyan változatát, amely közvetlenül egy állományba generálja le a dátumokat!

Linkek

Kapcsolódó forráskódok
Oktatói honlap
Vissza