Java (tsz. Javas)
A Java egy magas szintű, általános célú, objektumorientált programozási nyelv, amelyet kezdetben a Sun Microsystems fejlesztett ki, majd az Oracle vett át. Fő jellemzője az úgynevezett „Write Once, Run Anywhere” (WORA): a Java forráskódot a fordító bájtkódra fordítja, amelyet a Java Virtuális Gép (JVM) futtat, így a programplatform-függetlenül működik . A Java nyelv erős típusellenőrzéssel és szemétgyűjtővel (garbage collector) rendelkezik, amely a memóriakezelést segíti. Tipikusan Java-alkalmazás írásához egy osztály és egy public static void main(String args)
entry point metódus szükséges, amelyben kezdődik a program futása. Például egy egyszerű „Hello, világ!” program:
public class HelloVilag {
public static void main(String args) {
System.out.println("Hello, világ!");
}
}
Ebben a példában a public static void main(String args)
sor biztosítja, hogy a JVM megtalálja a program belépési pontját. A System.out.println
kiírja a konzolra a szöveget. A Java kód megírásához használhatunk változókat és literálokat különböző típusokban; a Java nyolc primitív típus támogat (pl. int
, double
, boolean
, char
stb.), továbbá nem primitív típusokként például a String
és tömbök (lásd később). A fordítás és futtatás között egy fordító (javac
) állítja elő a bájtkódot, amelyet a JVM futtat. A Java futtatásához általában JDK (Java Development Kit) szükséges, amely tartalmazza a fordítót, és JRE (Java Runtime Environment), amely a futtatókörnyezet. A JDK tartalmazza a JRE-t is.
A Java program alapvetően osztályokra bontva épül fel. Mindent egy osztály (class) definiál a Java-ban. Egy osztálynak lehetnek mezői (field, adatok) és metódusai (műveletek). Például:
public class Auto {
private String marka; // adattag (mező)
private int ev; // adattag
public Auto(String marka, int ev) { // konstruktor
this.marka = marka;
this.ev = ev;
}
public void indit() { // metódus
System.out.println(marka + " motorja beindult.");
}
}
Ebben az osztályban marka
és ev
mezők tárolják az adathalmazt, a konstruktor
inicializálja őket, az indit()
metódus pedig műveletet végez. Az objektum ennél fogva az osztály egy konkrét példánya (példányosítás, pl. Auto a = new Auto("BMW", 2020);
). Az osztály az objektumok „sablonja”, az objektum ennek a konkrét példánya. Az objektum orientált programozásban (OOP) alapfogalom, hogy az objektumok állapota (mezők értékei) és viselkedése (metódusai) együttesen alkotnak egy szoftveres egységet.
Változók és típusok. Minden változót deklarálni kell egy típusnévvel. Például int x = 5;
egy egész típusú változó létrehozása. A Java nyolc primitív típusát a nyelv definiálja: byte
, short
, int
, long
(egész számok), float
, double
(lebegőpontos számok), boolean
(igaz/hamis) és char
(egyetlen karakter). Ezen kívül vannak referencia típusok (pl. String
, tömbök, objektumokra mutató változók). Példa változódeklarációra és használatra:
int szam = 10;
double ertek = 3.14;
boolean logikai = true;
char betu = 'A';
String szoveg = "Példa";
System.out.println(szam + ", " + ertek + ", " + logikai + ", " + betu + ", " + szoveg);
Operátorok. A Java-ban megtalálhatók az alapvető aritmetikai (+
, -
, *
, /
, %
), összehasonlító (==
, !=
, <
, >
, <=
, >=
) és logikai (&&
, ||
, !
) operátorok.
Kontrollszerkezetek. A programokat alapértelmezetten sorban hajtjuk végre, de feltételes elágazásokkal és ciklusokkal tudjuk a vezérlést megváltoztatni. Az if
-else
szerkezet segítségével feltételek alapján futtathatunk kódrészeket. Példa:
int a = 7;
if (a > 0) {
System.out.println("Pozitív szám");
} else {
System.out.println("Nem pozitív szám");
}
A switch
utasítással több eshetőséget vizsgálhatunk enumerált értékek vagy konstansok alapján. A for
, while
és do-while
ciklusokkal ismétléseket valósítunk meg. Például egy for
-ciklus 0-tól 9-ig íratja ki a számokat:
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
A Java rendelkezik for-each ciklussal is, amely gyűjteményeken (tömbök, listák) egyszerűbben lép végig: for (Tipus elem : lista) { ... }
. A break
és continue
utasításokkal ciklus közbeni megszakítást vagy a következő iterációra ugorhatunk.
A vezérlési szerkezetek alapelve, hogy a kódrészek lineáris végrehajtása megszakítható elágazással vagy ismétléssel, így szelektíven futtathatók bizonyos kódblokkok.
Metódusok (függvények). Minden metódus egy feladat végrehajtásához kapcsolódó utasítások összessége. A metódus deklarációjakor megadjuk a visszatérési típust, a metódus nevét és paraméterlistáját. Példa:
public static int osszead(int a, int b) {
return a + b;
}
A fenti metódus két egész paramétert vár, összeadja őket, és visszaadja az eredményt (return
). Ha egy metódus nem ad vissza értéket, a visszatérési típus void
. A public
, private
stb. módosítók a láthatóságot szabályozzák; a static
jelző azt jelenti, hogy a metódus az osztályhoz tartozik, nem pedig példányhoz. Egy metódust az osztályból a neve és a paraméterek alapján hívunk: int eredmeny = osszead(3, 5);
.
A Java-ban a tömb (array) olyan konténer, amely egy adott típusú értékek rögzített számú gyűjteményét tartalmazza. A tömb méretét a létrehozáskor adjuk meg, és utána az nem változtatható meg. Példa egy tízes hosszúságú egész tömb létrehozására és elemeinek feltöltésére:
int tomb = new int; // Hozzáférés így: tomb
tomb = 100;
tomb = 200;
// ... és így tovább ...
for (int i = 0; i < tomb.length; i++) {
System.out.println("tomb = " + tomb);
}
A fenti kód egy 10 elemű int
tömböt hoz létre, majd például tomb=100
értéket ad az első elemnek. A tömb elemeinek indexelése nullától indul (az első elem indexe 0, az utolsóé a hossz mínusz egy). Gyakran for
- vagy for-each
ciklust használunk a tömbök bejárásához. A tömb létrehozására két lépés is lehetséges:
int tomb1; // deklarálás
tomb1 = new int; // létrehozás 5 elemnek
String nevek = {"Anna", "Béla", "Cili"}; // egy lépésben inicializálás
Többdimenziós tömböket is készíthetünk: egy kétdimenziós tömb tulajdonképpen tömbök tömbje. Például egy kétdimenziós int
tömb létrehozása és használata:
int matrix = {
{1, 2, 3},
{4, 5, 6}
};
System.out.println(matrix); // Második sor, harmadik oszlop: kiírja 6-ot
Ilyenkor két indexet (sor, oszlop) kell megadni. A Java-ban az int
és hasonló tömbök kezelését a standard for
-ciklussal vagy beágyazott for
-ciklusokkal könnyen elvégezhetjük.
A Java-ban a vezérlés alapértelmezés szerint fentről lefelé fut a programban. A döntési szerkezetek (if-then
, if-then-else
, switch
) segítenek feltételesen végrehajtani kódrészeket, míg a ciklusok (for
, while
, do-while
) ismétlődő végrehajtást tesznek lehetővé. A vezérlési szerkezetek segítségével szakíthatjuk meg és módosíthatjuk a program normál futását. A switch
-ben például egy változó értéke szerint több lehetséges ág közül választhatunk. A break
és continue
utasításokkal a ciklusok közben tudunk kilépni vagy következő iterációra ugrani. Emellett a return
utasítással egy metódusból is kiléphetünk és visszatérhetünk a hívó kódhoz.
A Java erősen objektumorientált nyelv: a programok az objektumok (példányok) közötti kölcsönhatáson keresztül modellezik a valós világot.
Osztályok és objektumok. Az osztály (class) a sablont (vázat) jelenti, amelyből objektumok származnak. Egy osztály definiálja a hozzá tartozó objektumok állapotát (mezőket, attributumokat) és viselkedését (metódusokat). Például egy Auto
osztály tartalmazhatja az autó márkáját és évét, valamint egy indít()
metódust. Egy osztály példányosításakor jön létre az objektum, például Auto a = new Auto("Audi", 2021);
. Az objektum attribútumai az osztály által előírt mezőket kapják meg.
Konstruktorok. Az osztályhoz tartozó konstruktorokat speciális metódusként definiáljuk, amelyek az objektum példányosításakor futnak le, és inicializálják a mezőket. A konstruktor neve megegyezik az osztály nevével, és nem ad vissza értéket. Például:
public class Ember {
private String nev;
public Ember(String nev) {
this.nev = nev;
}
}
Itt az Ember(String nev)
konstruktor beállítja az objektum nev
mezőjét. A Java automatikusan biztosít egy paraméter nélküli (default) konstruktort, ha nem definiálunk sajátot.
Öröklődés. A Java lehetővé teszi, hogy egy osztály örököljön egy másik osztálytól. A class Gyermek extends Szulo
szintaxisral az összes public és protected mezőt és metódust örökli a szülőosztályból. Ez elősegíti a kód újrafelhasználását és a hierarchikus szervezést. Például ha van egy általános Alakzat
osztályunk, abból származtathatjuk a konkrét Kör
vagy Téglalap
osztályokat. A gyermekosztály felülírhatja (override) a szülőosztály metódusait, amikor a viselkedést specializálni szeretnénk. Az @Override
annotáció használatával pedig a fordító ellenőrzi, hogy egy metódus valóban felülír-e egy szülői metódust.
Polimorfizmus. Az öröklődéssel együtt a polimorfizmus is fontos elem: egy gyermekosztály példánya a szülőosztályként is kezelhető. Így például ha van egy Alakzat
típusú változó, abban tarthatunk Kör
vagy Téglalap
objektumot is, és a megfelelő felülírt metódus fut le a tényleges típus alapján. A polimorfizmus lehetővé teszi, hogy közös interfészen keresztül többféle objektumot kezeljünk rugalmasan.
Interfészek és absztrakt osztályok. Az interfész egy „szerződés” két osztály között, amely csak metódusdeklarációkat tartalmaz. Egy osztály az implements
kulcsszóval kötelezi magát, hogy megvalósítja az interfész által előírt metódusokat. Például a Comparable
interfész compareTo
metódusaival szabályozhatjuk az objektumok sorrendjét. Egy absztrakt osztály (abstract class
) is lehet, amely részben definiálhat metódusokat, részben pedig elhagyja az implementációt a továbbörökítésre. A Java lehetővé teszi, hogy egy osztály egyszerre több interfészt megvalósítson. Az interfészekről és absztrakt osztályokról bővebben lásd: * *. (Itt például egy link lehetne, de jelen anyagban röviden említjük.)
Encapsuláció. Az adatok védelme és a moduláris tervezés miatt gyakran a mezőket private
-ként jelöljük, és nyilvános getter/setter metódusokkal érjük el őket. Ezáltal az osztály belső állapota biztonságosan kontrollálható anélkül, hogy a felhasználók közvetlenül hozzáférnének.
Összefoglalva: Java-ban objektum az a szoftveres egység, amely összevonja az adatokat és a műveleteket (metódusokat). A class az objektumtervezés vázát adja. Az objektumorientált fejlesztés előnyeit (például kódújrafelhasználás, áttekinthetőség, karbantarthatóság) többek között a DRY-elv (Don’t Repeat Yourself, azaz „ne ismételd önmagad”) is támogatja.
A Java-ban a futás közbeni hibákat (kivételállapotokat, exception-öket) try-catch blokkok segítségével tudjuk kezelni. Egy try
blokk belsejében helyezzük el a kódot, amely hiba (kivétel) előidézésére lehet hajlamos. Ha a kivétel bekövetkezik, a futás átugrik a megfelelő catch
ágra, ahol kezelhetjük a hibát. A finally
blokkot használhatjuk arra, hogy a hibától függetlenül végül mindig lefusson egy kódrészlet (pl. erőforrások felszabadítása). Példa:
try {
int szamok = {1, 2, 3};
System.out.println(szamok); // Ez IndexOutOfBoundsException-t vált ki
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Nem létező indexre próbáltunk hivatkozni.");
} finally {
System.out.println("Ez a blokk mindig lefut.");
}
Ebben a példában az ArrayIndexOutOfBoundsException
típusú kivételt külön kezeljük. Ha nem fogjuk el a kivételt, a program leáll hibával. A catch (Exception e)
általánosabb formában az összes kivételt elkapja (de érdemes konkrétabbat is használni). A throw
kulcsszóval saját kivételkevertelehetőség is létrehozható egyénileg. A Java kivételkezelő mechanizmusa megakadályozza a program váratlan összeomlását és lehetőséget ad hibaüzenetek vagy helyreállító rutinok futtatására. A try-catch szerkezet teljes jellemzése megtalálható a W3Schools-on.
A beépített tömbök rögzített hosszúságúak, de gyakran szükség van dinamikusan bővülő listákra. Erre a célra szolgálnak a java.util
csomagban elérhető gyűjtemények (collections). A legfontosabbak:
List: egy sorozatszerű halmazt modellez. Az ArrayList
implementáció például dinamikusan bővíthető tömböt valósít meg. Az ArrayList
használatának előnye, hogy elemeket tetszőleges pozícióban hozzáadhatunk vagy eltávolíthatunk. Példa:
ArrayList<String> lista = new ArrayList<String>();
lista.add("Alma");
lista.add("Béka");
System.out.println(lista.get(1)); // Kiírja: Béka
Az ArrayList
osztály rugalmas hosszúságú tömböt valósít meg – szemben a beépített tömbökkel, amelyek mérete nem módosítható futásidőben. Az ArrayList
-hez az add()
, get()
, set()
, remove()
stb. metódusokat használjuk.
Set: halmazt reprezentál. Például a HashSet
nem tart meg sorrendet, és csak egyedi elemeket enged meg. (Pl. HashSet<String>
használata akkor ajánlott, ha nem kell két egyforma elem.)
Map: kulcs–érték párokat tárol. A legismertebb implementáció a HashMap
. A HashMap
egy olyan kollekció, ahol egy tetszőleges típusú kulcs (String
, Integer
, stb.) segítségével érhetjük el az adott kulcshoz rendelt értéket. Példa:
HashMap<String, Integer> szotar = new HashMap<String, Integer>();
szotar.put("egy", 1);
szotar.put("kettő", 2);
System.out.println(szotar.get("egy")); // Kiírja: 1
A HashMap
tehát kulcs-érték párokat tárol, és a put()
metódussal töltjük fel, a get(key)
segítségével lekérhetjük az értéket egy kulcshoz.
A Java 5 óta a generikus típusok lehetővé teszik, hogy például ArrayList<String>
vagy HashMap<String, Integer>
formában bonyolultabb típusparaméterezést adjunk meg, ezáltal futásidőben és fordításkor is típusbiztonságot nyújtanak. A gyűjteményekről és generikusokról több információ található a Java gyűjtemény-keretrendszer dokumentációjában.
Az annotációk (annotations) metaadatok a programban, amelyek kiegészítő információt hordoznak a fordító vagy egyéb eszközök számára, de önmagukban nem módosítják a kód működését. A Java 1.5-től használhatunk beépített annotációkat, illetve saját definiált annotációkat. Az annotációk az @
jellel kezdődnek. Néhány gyakori beépített annotáció:
@Override
: ezt a metódus elé helyezve jelzi, hogy a metódus a szülőosztály egy metódusát írja felül. Hasznos, mert ha mégsem valósul meg a felülírás (pl. elírás miatt), a fordító figyelmeztetést ad.@Deprecated
: arra szolgál, hogy jelezzük, egy osztály vagy metódus elavult, ne használja a felhasználó (a fordító warningot ad).@SuppressWarnings
: bizonyos fordítói figyelmeztetések elnyomására használható.Annotációkat osztályok, metódusok, változók stb. elé tehetünk. Például:
@Override
public String toString() {
return "Ez egy objektum!";
}
Itt az @Override
jelzi, hogy a toString()
metódus felülírja az Object
alapértelmezett toString()
metódusát. Egyéni annotációkat is létrehozhatunk az @interface
kulcsszó használatával (ez már előrehaladottabb téma). Az annotációk főként keretrendszerekben (pl. JUnit, Spring) kapnak szerepet, illetve a fordítási időben (annoációfeldolgozó eszközökkel) értelmezhetők. Röviden: az annotációk a program metaadatait hordozzák a kódban, lehetővé téve eszközök (pl. fordító, futtatható keretrendszer) számára a speciális kezelésüket.
A Java egykor támogatta az appletek futtatását webböngészőben: az applet egy kis Java-program volt, amely HTML-oldalba ágyazva futott. Appletek készítéséhez az java.applet.Applet
vagy javax.swing.JApplet
osztályt örökítettük. Példa egyszerű appletre:
import java.applet.Applet;
import java.awt.Graphics;
public class HelloApplet extends Applet {
public void paint(Graphics g) {
g.drawString("Hello, Applet!", 20, 20);
}
}
Azonban fontos tudnivaló, hogy a Java appletek technológiája ma már elavultnak számít: a JDK 9-es verziótól kezdve az appletek és a kapcsolódó böngészőplugin támogatása elavulttá vált, és később teljesen kivezetésre került a Java-ból. Ugyanis a modern böngészők nem támogatják a Java plugin futtatását. Helyette ma a Java asztali alkalmazásoknál általában Swing vagy JavaFX GUI keretrendszereket használunk, appleteket pedig nem javasolt készíteni.
A párhuzamos programozás (multithreading) segítségével egyszerre több végrehajtási szálat futtathatunk. Java-ban a java.lang.Thread
osztály és a java.lang.Runnable
interfész az alapja a szálak indításának. Két fő módja van új szál létrehozására:
Thread
osztályból: új osztályunkat kiterjesztjük extends Thread
, és felülírjuk a run()
metódust.Runnable
interfész implementálása: készítünk egy Runnable
implementáló osztályt, benne a run()
metódussal, majd egy Thread
objektumot hozunk létre azzal.Példa Runnable
-lel:
class Szamolo implements Runnable {
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
}
}
public class ThreadsPelda {
public static void main(String args) {
Thread t1 = new Thread(new Szamolo(), "Sz\u00e1mol\u00f3 1");
Thread t2 = new Thread(new Szamolo(), "Sz\u00e1mol\u00f3 2");
t1.start();
t2.start();
}
}
Ebben a kódban két szál indul, mindegyik kiírja 1-től 5-ig a számokat. A Thread.start()
hívja meg aszinkron a run()
metódust. Fontos, hogy a kettő szál versenyezhet az erőforrásokért, ezért alkalmazhatunk szinkronizálást (synchronized
kulcsszóval) kritikus szakaszokra, hogy egy időben csak egy szál futhasson ott. A szálkezelésről általánosan: a Runnable
interfész segítségével határozzuk meg a végrehajtandó feladatot, és ezt egy szál objektumában futtatjuk. A szálak együttműködéséhez használhatók még az executors és egyéb concurrency keretrendszerek (például ExecutorService
, Future
), de egy bevezető anyagban a Thread
és Runnable
alapok is megmutatják a mechanizmust.
A Java beépített java.net
csomagja lehetővé teszi hálózati műveletek végrehajtását. Magas szinten a URL
és URLConnection
osztályokkal HTTP-kéréseket is intézhetünk. Például:
import java.net.*;
import java.io.*;
URL url = new URL("http://www.example.com/");
BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
String sor;
while ((sor = in.readLine()) != null) {
System.out.println(sor);
}
in.close();
Itt a URL
és URLConnection
magas szintű mechanizmust nyújt az internetes erőforrások eléréséhez (HTTP GET lekérés a példa).
Alacsony szinten socket-ekkel írhatunk kliens-szerver alkalmazásokat. A Socket
osztály a kliens oldali kapcsolatot képviseli, a ServerSocket
a szerver oldali lyukas portot (szerver). Például egy egyszerű szerver-szál létrehozása:
ServerSocket szerver = new ServerSocket(7777);
Socket kliens = szerver.accept();
PrintWriter ki = new PrintWriter(kliens.getOutputStream(), true);
ki.println("Szia, kliens!");
kliens.close();
szerver.close();
Itt a szerver a 7777-es porton vár, accept()
hívással blokkol, amíg egy kliens nem kapcsolódik. Amikor csatlakozik egy kliens, visszakapjuk a Socket
-et, amellyel adatokat tudunk írni/olvasni. Ezzel a módszerrel TCP alapú kapcsolatok hozhatók létre. A hálózati kommunikációra a Java TCP-t használ, így megbízható adatátvitelt kapunk (az adatok sorrendje és megérkezése garantált). A részletes socket-programozásról lásd a Java hálózati tutorialját. Összességében a hálózatkezelés Java-ban lehetővé teszi kliens és szerver oldali műveletek írását, legyen szó HTTP-kapcsolatokról (URL
), vagy tetszőleges adatcseréről socket szinten.
A Java reflexió segítségével egy program futás közben képes magát megvizsgálni: lekérdezhetjük és módosíthatjuk egy osztály, metódus vagy mező tulajdonságait dinamikusan futásidőben. A java.lang.reflect
csomag osztályai (például Class
, Method
, Field
) szolgálnak erre. Például leellenőrizhetjük egy osztály összes metódusának nevét:
Class<?> cls = String.class;
java.lang.reflect.Method metodusok = cls.getDeclaredMethods();
for (Method m : metodusok) {
System.out.println(m);
}
Ez kiírja a String
osztály összes metódusát, paraméter- és visszatérési típussal együtt. A fenti példa a Class.forName
és getDeclaredMethods
módszerekkel lekéri a metódusok listáját – az eredmény például a toString()
vagy equals()
metódus definícióját adja vissza. Másrészt a reflexióval példányosíthatunk osztályokat futásidőben (cls.newInstance()
), és módszereket hívhatunk dinamikusan (m.invoke(obj, args)
). A reflexiós mechanizmus hasznos keretrendszerekben (például JUnit tesztek, JavaBeans fejlesztése) és bonyolultabb programozási feladatokban, ahol futásidőben kell vizsgálódni a program típusairól.
Java-ban grafikus alkalmazásokat a Swing vagy a modernebb JavaFX keretrendszerrel fejleszthetünk. A Swing része a JDK-nak, könnyen használhatók az előre definiált komponensek (ablakok, gombok, listák stb.). Egy alap Swing-ablak létrehozása egyszerű példával:
import javax.swing.*;
public class AblakPeldaja {
public static void main(String args) {
JFrame frame = new JFrame("Példa ablak");
frame.setSize(300, 200); // Ablak mérete
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JButton gomb = new JButton("Kattints rám");
frame.getContentPane().add(gomb); // Gomb hozzáadása az ablakhoz
frame.setVisible(true); // Ablak megjelenítése
}
}
A fenti kódban egy JFrame
ablakot készítünk címmel és mérettel, majd hozzáadunk egy JButton
-t a tartalmi panelhez. Végül a setVisible(true)
hívással megjelenítjük az ablakot. A Swing-ben a komponenseket tartalmazó ablak (frame) a toplevel konténer, és a setDefaultCloseOperation
metódussal mondjuk meg, hogy a bezárás EXIT_ON_CLOSE
esetén leállítsa a programot. Ezután tetszőleges Swing-komponensekkel (gombok, címkék, szövegmezők, listák, stb.) építhetünk fel bonyolult GUI-kat. A JavaFX a Swing utódja, de alapelveiben hasonló (itt egyszerűen csak megemlítjük, hogy modernebb GUI fejlesztés is lehetséges Java-ban).
Ez az áttekintés a Java nyelv legfontosabb elemeit mutatta be: a nyelvi alapoktól (változók, vezérlés, típusok) az objektumorientált elveken (osztályok, öröklés, interfészek) és köztes fogalmakon (exception-kezelés, kollekciók, annotációk) át a párhuzamos és hálózati programozásig, valamint a GUI fejlesztésig. Minden témakörben rövid kódpéldák illusztrálták a gyakorlati használatot. A Java széles körben használt, állandóan fejlődő nyelv; új verziók időről időre új nyelvi elemeket (pl. modulok, lambda kifejezések) vezettek be. Az itt összegyűjtött tudnivalók alapot adnak a Java alapos megismeréséhez, és segíthetnek abban, hogy a nyelvi jellemzők gyorsan alkalmazhatóak legyenek akár tanulási céllal, akár valódi projektekben.