Kihagyás

11. gyakorlat

A gyakorlat anyaga

Beolvasás standard inputról

Ahogy az 1. gyakorlaton láttuk, a beolvasáshoz egy új Scanner objektumot hozunk létre, aminek átadjuk a System osztály in adattagját. A Scanner sokféle bemenetről tud beolvasni (például fájlokból is), ezért vár a konstruktora egy InputStream objektumot. Ez lesz esetünkben a System.in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import java.util.Scanner;

public class Beolvasas {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.println("Hello! Hogy hívnak?");
        String nev = sc.nextLine();
        System.out.println("Hello " + nev + "! Hany eves vagy?");
        int kor = sc.nextInt();
        System.out.println("Hello " + nev + ", aki " + kor + " eves.");
    }
}

Ha egy osztályon belül több metódusban is használni szeretnénk a standard inputról olvasó Scanner-t, akkor érdemes egy static adattagban eltárolni, felesleges minden használatkor új példányt létrehozni belőle.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.util.Scanner;

public class Main {
    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        scanner.nextLine();
        method1();
    }

    private static void method1() {
        scanner.nextLine();
    }
}

Fájlkezelés

Javaban a fájlkezelés is objektumokon keresztül történik, azonban mivel a fájlok programok között megosztott erőforrások, kicsit eltérően kell velük bánni, mint a "hagyományos" objektumokkal. Amikor egy fájlkezelő objektum létrejön, akkor az az operációs rendszeren keresztül megnyitja az adott fájlt írásra vagy olvasásra. Amíg a programunk nyitva tart egy fájlt, addig annak az elérése más programból korlátozott lehet. Például ha egy fájlt olvasásra nyitunk meg, akkor más program nem tudja törölni, vagy akár módosítani sem azt a fájlt. Ezért fontos, hogy amint nincs szükségünk egy fájlra, rögtön "becsukjuk" azt, minél rövidebb ideig foglalva azt. Természetesen, amikor a Garbage Collector felszabadítja a fájlkezelő objektumunkat, akkor az automatikusan becsukja a fájlt, ezt azonban nem tudjuk előre, hogy mikor fog megtörténni, akár jóval később, mint amikor ténylegesen utoljára használtuk a fájlt. Ha írtunk a fájlba, előfordulhat, hogy eddig a pillanatig a tartalma nem is kerül kiírásra a lemezre, bent marad a memóriában (pufferben).

Fájlkezelés során különböző hibák is előfordulhatnak, melyek kezelésére szintén oda kell figyelnünk. Ilyen lehet például, ha nem létezik az olvasásra megnyitandó fájl, nincs jogunk írni egy fájlba, betelik a lemez, stb. Ezekben az esetekben természetesen kivételeket dobnak a fájlkezelő objektumok metódusai, amiket a mi dolgunk kezelni.

A fájlkezeléssel kapcsolatos osztályok a java.io package-ben találhatóak, mivel I/O, azaz input/output műveleteket valósítanak meg.

A régi módszer

Fájl olvasása

Az alábbi példában a már ismert java.util.Scanner osztály segítségével olvasunk be egy komplett fájlt soronként, és írjuk ki a tartalmát az alapértelmezett kimenetre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = null;
        try {
            scanner = new Scanner(new File(args[0]));
            while(scanner.hasNextLine()) {
                System.out.println(scanner.nextLine());
            }
        } catch (IOException e) {
            System.err.println("Hiba történt: " + e.getMessage());
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
    }
}

Elrettentőnek tűnhet, kicsit talán az is, azonban a példán keresztül érthetjük meg, hogy hányféle helyen keletkezhet hiba, amikor fájlokkal dolgozunk. Gyakorlatilag bárhol. Menjünk végig sorról-sorra a kódon!

Először létrehozunk egy Scanner típusú referenciát, aminek azonban a null kezdőértéket adjuk, ugyanis a példányosítást már egy try blokkba kell ágyazzuk, hiszen maga a konstruktor is dobhat kivételt (például ha a fájl nem létezik), és később a finally blokkban szeretnénk használni a referenciát. A példában az első parancssori paramétert használtuk fel, aminek egy fájl elérési útjának kell lennie, ebből egy java.io.File objektumot hozunk létre, amit közvetlenül átadunk a Scanner konstruktorának. Ha sikerült megnyitni a fájlt (a konstruktor nem dobott kivételt), tovább haladunk. A while ciklus feltételében felhasználjuk a Scanner.hasNextLine() metódust, ami egy boolean értékkel tér vissza (értelemszerűen true ha van még a fájlból, false ha a végére értünk), majd a ciklismagban System.out.println() metódussal kiírjuk a kimenetre a nextLine() hívással beolvasott sort.

Megjegyzés: A File osztály egy fájlrendszerbeli objektumot képvisel az elérési útjának ismeretében. Tud róla információkat adni (pl. mérete, módosítás ideje, jogosultságok, stb.), illetve bizonyos műveleteket végezni rajta (előbbi tulajdonságok módosítása, átnevezés, üres fájl létrehozása, mappa létrehozása, törlés, stb.).

A catch blokkban lekezeljük a példányosítás vagy olvasás során esetlegesen keletkezett hibákat, amelyek a java.io.IOException osztály leszármazottai lesznek (pl. FileNotFoundException, AccessDeniedException). Azt hihetnénk, hogy kész vagyunk, azonban (látva a kódot is) sajnos koránt sem. Ugyanis ha valamely olvasás során kapunk hibát (például a fájl közepén járunk amikor hirtelen elveszítjük a fájl olvasásának jogát), akkor a close() metódus nem kerülne meghívásra. Az ilyen esetek miatt írunk egy finally blokkot is a try-hoz, amelyben amennyiben egyáltalán sikerült példányosítani a Scanner objektumot, lezárjuk azt, így felszabadítva a lefoglalt erőforrást, amint arra nincs szükségünk.

Fájl írása

Fájl írása nagyon hasonlóan történik mint a beolvasás. A java.io.PrintStream osztályt fogjuk használni, ami már ismerős lehet, hiszen a System.out adattag is ilyen típusú. A példában az első argumentumként kapott fájlba fogjuk kiírni a többi argumentumot.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import java.io.*;

public class Main {
    public static void main(String[] args) {
        PrintStream printStream = null;
        try {
            printStream = new PrintStream(args[0]);
            for (int i = 1; i < args.length; ++i) {
                printStream.println(args[i]);
            }
        } catch (IOException e) {
            System.err.println("Hiba történt " + e.getMessage());
        } finally {
            if (printStream != null) {
                printStream.close();
            }
        }
    }
}

A szerkezet gyakorlatilag megegyezik a beolvasásnál látottal.

Try-with-resources

A fenti két példát Java 7 óta tömörebben is le tudjuk írni a try-with-resources szerkezet segítségével. Ez egy "tuningolt" try blokk, ami egy vagy több erőforrást is kezel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import java.io.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(new File(args[0]))) {
            while(scanner.hasNextLine()) {
                System.out.println(scanner.nextLine());
            }
        } catch (IOException e) {
            System.err.println("Hiba történt: " + e.getMessage());
        }
    }
}

Lényege, a hogy a zárójelben deklarált változó(k) csak a scope-jában lesznek elérhetők (szemben a fentebbi kóddal, ahol a metódus scope-ba került), illetve automatikusan le is zárja őket, amint elhagyjuk a blokkot (a háttérben egy olyan finally blokkot generál a try végére, amilyet mi is írtunk fentebb). Működéséhez az erőforrásnak implementálnia kell az AutoCloseable interfészt (ami egyetlen close() metódust vár el), ahogy azt az összes beépített IO osztály meg is teszi.

Lássunk egy példát több erőforrást is kezelő try-with-resources-re. A következő kód egy fájl tartalmát másolja egy másikba soronként.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import java.io.*;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        try (
            Scanner scanner = new Scanner(new File(args[0]));
            PrintStream printStream = new PrintStream(args[1])
        ) {
            while(scanner.hasNextLine()) {
                printStream.println(scanner.nextLine());
            }
        } catch (IOException e) {
            System.err.println("Hiba történt: " + e.getMessage());
        }
    }
}

Ahogy láthatjuk, a try zárójelében tetszőleges számú erőforrás-deklarációt tehetünk pontosvesszővel elválasztva. Ha bármelyik megnyitásakor kivétel dobódik, a már megnyitottak automatikusan lezárásra kerülnek, ami a régi módszerrel különösen macerás és csúnya volt. A blokkban a már látott while ciklus dolgozik, ezúttal a System.out helyett az általunk megnyitott printStream objektumot használva kiírása.

Lambda kifejezések

Egy grafikus felülettel ellátott alkalmazás esetében, amikor egy gombra eseménykezelőt írunk (erre példa, a 08-Programozas-I.pdf fájlban, egy másik kód itt), akkor egy névtelen interfész-implementációt készítünk, ami nagyban nehezíti a kód olvashatóságát, átláthatóságát. Mindemellet rengeteg felesleges kódrészlet is bekerül a kódunkba, amelyet Java 8-tól elkerülhetünk könnyedén, lambda kifejezések használatával.

A lambda függvények gyakorlatilag olyan névtelen metódusok, amelyet ott írunk meg, ahol használni szeretnénk. Gyakorlatilag akkor lehet haszna, ha például egy olyan interfészt szeretnénk helyben implementálni, aminek csak egy metódusa van, vagy például kollekciók hatékony, gyors, átlátható bejárásakor. Szóval egy interfész-implementációt tömörebben, gyorsabban, átláthatóbban írhatunk meg, mint eddig.

Mivel jelen gyakorlaton nem foglalkozunk Java GUI-val, így egy másik példán keresztül ismerjük meg őket, mégpedig a kollekciók segítségével. Először egy listát (de halmazon is ugyanígy működne) járunk be, majd pedig egy kulcs-érték párokból álló Map objektumot.

Egy lambda kifejezés szintaxisa: (paraméter1, paraméter2) -> utasítás, vagy utasítás blokk. A paraméterek típusát nem kell kiírnunk (de kiírhatjuk őket, ha szeretnénk). Egy paraméter esetén elhagyhatjuk a paraméterek körüli zárójelet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Main {

    public static void main(String[] args) {
        List<String> szinek = new ArrayList<>();
        szinek.add("Kék");
        szinek.add("Zöld");
        szinek.add("Piros");
        szinek.add("Fekete");
        szinek.add("Sárga");
        szinek.add("Narancs");

        szinek.forEach(szin -> System.out.println(szin));
    }
}

Láthatjuk, hogy mennyivel egyszerűbb használni, mint például egy hagyományos for ciklust. Amennyiben több utasítást használunk, akkor a megszokott módon kapcsos-zárójelek közé kell tenni az utasításokat a nyíl(->) után.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Main {

    public static void main(String[] args) {
        List<String> szinek = new ArrayList<>();
        szinek.add("Kék");
        szinek.add("Zöld");
        szinek.add("Piros");
        szinek.add("Fekete");
        szinek.add("Sárga");
        szinek.add("Narancs");

        szinek.forEach(szin -> {
            if (szin.charAt(0) > 'O') {
                System.out.println(szin);
            }
        });
    }
}

A fenti példában végigmegyünk a listán, és megnézzük, melyik szín kezdődik egy 'O' után következő betűvel, és azokat írjuk ki az alapértelmezett kimenetre. Jelen helyzetünkbe talán ez nem tűnik nagy dolognak, mert sima iterátorral, vagy for ciklussal is bejárhattuk volna a listát, körülbelül ugyanennyi lenne kódban.

Azonban nézzük meg ezt a bejárást egy Map esetében, ahol már érezhetően egyszerűsödik a helyzetünk. (Csak hogy az előadáson látott GUI elemek eseménykezelőjéről ne is beszéljünk.)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Main {

    public static void main(String[] args) {
        Map<String, Integer> szinek = new HashMap<>();

        // Megkérdeztünk 1000 embert, kinek mi a kedvenc színe, ezt tároljuk le
        // ebben a mapben.
        szinek.put("Kék", 320);
        szinek.put("Zöld", 200);
        szinek.put("Sárga", 80);
        szinek.put("Barna", 95);
        szinek.put("Citrom", 105);
        szinek.put("Piros", 75);
        szinek.put("Lila", 125);

        szinek.forEach((szin, ertek) -> System.out.println(szin + " szín " + ertek + " ember kedvence."));
    }
}

Ahogy már láttuk, ha több utasítást szeretnénk végrehajtani, akkor kapcsos zárójelek közé kell tennünk az utasításokat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Main {

    public static void main(String[] args) {
        Map<String, Integer> szinek = new HashMap<>();

        // Megkérdeztünk 1000 embert, kinek mi a kedvenc színe, ezt tároljuk le
        // ebben a map-ben.
        szinek.put("Kék", 320);
        szinek.put("Zöld", 200);
        szinek.put("Sárga", 80);
        szinek.put("Barna", 95);
        szinek.put("Citrom", 105);
        szinek.put("Piros", 75);
        szinek.put("Lila", 125);

        szinek.forEach((szin, ertek) -> {
            if (ertek > 100) {
                System.out.println(szin + " szín " + ertek + " ember kedvence.");
            } else {
                System.out.println(szin + " szín nem túl sok ember kedvence.");
            }
        });
    }
}

Látszik, hogy a fent ismertetettekkel ellentétben lambda kifejezéssel nagyon egyszerűen, átláthatóan járhatunk be egy map-et is. Remélhetőleg mindenki kedvet kapott a lambdák további megismeréséhez, nekik ajánljuk a következő linkeket:

Oracle Lambda Expressions

Java 8 - Lambda Expressions

Lambda Expressions in Java 8

Videók

Kapcsolódó linkek


Utolsó frissítés: 2021-05-04 07:53:32