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 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.");
}
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:
Adatforrás: java.io.FileInputStream , java.io.FileReader , java.io.StringReader .
Adatnyelő: java.io.FileOutputStream , java.io.FileWriter , java.io.StringWriter , java.io.PrintWriter .
Szűrők: meglevő csatornához kapcsolható további funkcionalitás, amellyel újabb csatornák hozhatóak létre: java.io.BufferedInputStream , java.io.SequenceInputStream , java.io.PipedInputStream , java.io.DataInputStream , java.io.BufferedReader . Ezek az osztályok eleve valamilyen adatforrást, vagy -nyelőt várnak paraméterként a konstruktoraikban, annak a viselkedését változtatják meg. Például pufferelést tesznek lehetővé.
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:
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
|