Futtatni vagy
fejleszteni?
- JRE = Java Runtime
Environment, csak futtatni
- JDK = Java Development Kit,
fejleszteni is, de ebben van egy JRE is
Fejleszteni,
de milyen környezetben?
Rengeteg IDE-s
fejlesztőeszköz van a világon, NetBeans, Eclipse, IntelliJ, Anjuta
(és még az AndroidStudio is ide tartozik), amik számos hasznos
dologgal segítenek minket, de közös nevezőként akár egy sima
szövegszerkesztővel és a parancssoros fordítóval és futtatóval
is el lehet boldogulni.
Pláne nagyobb és
összetettebb rendszereknél hasznosak az IDE-k, a választás ízlés
dolga, de az itteni példák bárhol működni fognak.
A
futtatási koncepció
A Java fordított
nyelv (szemben az értelmezett nyelvekkel), azaz a forráskódból
egy bináris program fog készülni, és ezt lehet majd végrehajtani.
Azért viszont, hogy ez különböző platformok között hordozható
legyen, nem valamely architektúra gépi kódjára fordul le, hanem
egy célspecifikus virtuális gépére (JVM - Java Virtual Machine),
és ennek a virtuális gépnek van kvázi emulátora majd' minden
architektúrához.
Maga a JVM egyébként
kvázi szabvánnyá nőtte ki magát, olyannyira, hogy vannak egyéb
nyelvek is, amik JVM binárisra fordulnak, annak ellenére, hogy
közük sincs a Javához :D.
A JRE tulajdonképpen
a JVM-et tartalmazza, és emellett még a nyelvhez adott támogató
könyvtárat (ez tud olyasmiket, mint pl. file-kezelés,
adatszerkezetek, stb.), meg a dokumentációt.
A forrásprogramok
hagyományos kiterjesztése a .java, a lefordított binárisoké a
.class, illetve hogy könnyebb legyen az összetartozó .class-ainkat
(package) együtt kezelni, ezeket .jar (Java Archive) -okba tudjuk
összetömöríteni.
A .jar amúgy
technikailag .zip, csak a .class-okon kívül egyéb meta-adatokat is
tartalmaz, mint pl. a package neve, verziója, opcionálisan
digitális aláírás, stb.
(Lesz még egy fajta
file, a .properties, ami 'kulcs: érték' formában utólagosan a
user által módosítható beállításokat tartalmazhat, és amihez
a standard library jó támogatást nyújt, így nem kell nekünk
saját config-file-kezelést megírnunk. De erről majd a maga
idejében.)
A
fejlesztési koncepció
Az az alapgondolat,
hogy maga a Java csak egy nyelv, bizonyos tulajdonságokkal és
képességekkel, de a beépített nyelvi elemek minden
specifikusságtól mentesek legyenek,
illetve ehhez legyenek
gazdag és sokrétű, előre megírt osztály-könyvtárak, amik
tartalmaznak rengeteg olyan dolgot, ami a sima nyelvi elemekkel
megvalósítható, és amire általában szükség szokott lenni.
Tehát pl. az elemi
típusok (karakter, egész, logikai, stb.), az adat-konstrukciók
(tömb, struktúra/osztály), a programkonstrukciók (elágazások,
ciklusok, osztályok koncepciója) azok nyelvi elemek, de a String
típus, a File, a List és ilyesmik már külső osztálykönyvtárakból
jönnek.
Elvileg ezek nélkül,
pusztán a nyelvi elemekkel is lehetne dolgozni, csak kár volna
újraírni és újratesztelni mindent :D
Az osztálykönyvtárak
hierarchikusan vannak szervezve, ahol az egyes szintek neveit ponttal
választjuk el, azaz pl. a 'java.*' az alap Java osztálykönyvtár,
ezen belül a 'java.io.*' a be/kimenettel kapcsolatos dolgok helye, a
'java.util.*' a mindenfél segéd-cuccé, az 'org.apache.http.*' az
Apache által adott osztálykönyvtárban a HTTP protokollal
kapcsolatos dologké, és így tovább.
Az ütközések
elkerülése végett a 'java.*' és a 'javax.*' a JRE-ben gyárilag
benn lévő osztálykönyvárak, mindeni más pedig legyen szíves a
megfordított domain nevével kezdeni a könyvtárainak a neveit
(azaz 'apache.org' -> 'org.apache.*').
Melyik verzióban
mi minden van?
- Java SE = Standard Edition,
a nyelv maga, meg a standard osztály-könyvtár
- Java ME = Micro Edition, kb.
ami nem kötődik GUI-hoz, ill. a szűk minimum
- Java EE = Enterprise Edition,
web services, servlet, jsp, ejb, persistence, transaction, minden,
ami csak van
Mi most a Java SE-vel
fogunk kezdeni, a nyelv bemutatásához az pont alkalmas lesz.
A JDK
telepítése
Ami tehát nekünk
most kelleni fog mindenképpen: Java SE Downloads / JDK
Települ
pl. c:\Program Files\Java\jdk1.8.0_111\ alá, a benne lévő
fejlesztőeszközök az ez alatti bin\-ben lesznek, a JRE pedig az ez
alatti jre\-be kerül.
Tehát a PATH-hoz
érdemes (pontosvesszőkkel elválasztva) hozzáadni ezt a c:\Program
Files\Java\jdk1.8.0_111\bin-t és a c:\Program
Files\Java\jdk1.8.0_111\jre\bin-t.
Ezt ellenőrizhetjük
is:
C:\>java
-version
java
version "1.8.0_111"
Java(TM)
SE Runtime Environment (build 1.8.0_111-b14)
Java
HotSpot(TM) Client VM (build 25.111-b14, mixed mode)
C:\>javac
-version
javac
1.8.0_111
C:\>jar
Usage:
jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C
dir] files ...
Options:
-c
create new archive
...
Ezzel a három
leg-alapvetőbb parancsot is megemlítettük:
A 'java' a JVM
futtató parancsa, amivel a .class file-okat fogjuk végrehajtani
A 'javac' a
fordító, ami a .java forrásokból .class binárisokat készít
A 'jar' a
csomagoló, amivel .class binárisokat foghatunk össze .jar
archívumokká
Az
első program
Csináljunk egy
próba-könyvtárat és tegyük le bele az első teszt-programunkat
HelloWorld.java néven:
class
HelloWorld
{
HelloWorld(String[]
args) {
System.out.println("Hello
World!");
for
(String s : args) {
System.out.println("arg:
" + s);
}
}
public
static void main(String[] args) {
new
HelloWorld(args);
}
}
Ezt
fordítsuk is le, és futtassuk:
C:\Temp\dev_java>javac
HelloWorld.java
C:\Temp\dev_java>java
HelloWorld alma korte szilva
Hello
World!
arg:
alma
arg:
korte
arg:
szilva
Nos,
valamit már csinál, viszont ahhoz, hogy el tudjuk mondani, hogy mi
mit csinál és miért úgy, ahhoz először pár nyelvi koncepciót
meg kell néznünk, úgyhogy ezt most tegyük félre egy kicsit.
Nyelvi
koncepciók
Mindenek előtt, a
Javában minden, kívülről elérhető típusnak saját file-ban
kell helyet kapnia, és a file elérési útvonala 1-1 viszonyban van
a típus nevével, azaz a 'HelloWorld' típusnak a
'HelloWorld.java'-ban van a helye.
Emellett még lehetnek
egyéb segéd-típusok is ebben a file-ban, de azok kívülről nem
lesznek elérhetőek.
Elemi típusok és
operátorok
A
Java struktúrált, és azon belül objektum-orientált nyelv, erősen
típusos, tehát az adatokat valamilyen típusú változókban
tarthatjuk, ezekből adat-konstrukciókat építhetünk fel, a
végrehajtás pedig elemi utasításokból épül fel, amikből
program-konstrukciókat szervezhetünk. (Tehát nem pl. SQL vagy
Prolog :D...)
A
nyelv minden eleme kis/nagybetű-érzékeny, azaz az 'ize', az 'IZE'
és az 'Ize' három különböző dolog. Szokás a változókat
kisbetűvel kezdeni, az összetett típusok neveit naggyal, a
konstansokat pedig végig naggyal írni, így már ránézésre is
láthatjuk, hogy mi micsoda.
Az
elemi típusok:
byte
(1 byte-os előjeles egész, -128..127), pl. -42, 0x2a,
short
(2 byte-os, -32768..32767)
int
(4 byte-os, -2G ..2G - 1)
long
(8 byte-os, -sok .. sok -1)
float
(4 byte-os valós, kb. 7 jegy pontossággal), pl. 3.1415f, 6.022e23
double
(8 byte-os valós, kb. 16 jegy pontossággal), pl. 3.1415, 1.38e-23
boolean
(logikai), pl. true, false
char
(unicode karakter), pl. 'а'
, 'Я',
'\n' (sorvégjel)
'\u263a'
(smiley)
És
mi a helyzet a stringekkel, ha már a példában használtuk is?
Nos,
a 'String' az összetett típus, a rendes polgári neve amúgy
'java.lang.String', ami azt jelenti, hogy ő a 'java.lang'
osztálygyűjteményben lakik, és ebből azt is láthatjuk, hogy
ezen gyűjtemény annyiban speciális, hogy az ő típusaira röviden
is hivatkozhatunk.
Visszatérve,
amikor egy adott típusból változót szeretnénk megadni, akkor a
kb. a C szintaxis szerint történik:
int
x, y, sum; // ez itt komment a sor végéig
boolean
isValid, containsSpace; /* ez pedig a bezárásáig
*/
Tehát
a változó-deklaráció a 'típusnév var1, var2, ..., varN;'
formában történik, a változónevek pedig nem kezdődhetnek
számmal, nem tartalmazhatnak szóközt vagy valamely
operátor-karaktert, nem egyezhetnek nyelvi kulcsszavakkal, egyébként
bármit tartalmazhatnak, akár arab karaktereket is.
Kommenteket
a forráskódba a '//'-től sor végéig ill. '/*'-tól '*/'-ig
terjedő formával tehetünk - pont mint C++-ban.
Szokás
a változókat kisbetűvel kezdeni és beszédes neveket adni nekik,
amelyeknél az összetett neveket camel case-szerűen tagoljuk, azaz
az első betű kicsi, a többi szókezdő viszont nagy. (Elsőre
furcsa, de egy idő után rááll a szem...)
Az
egyes elemi típusokból készíthetünk tömböket, amik viszont
külön típusnak
számítanak,
azaz
float[]
sampleWeight;
Fontos,
hogy ezzel -az elemi típusú változókkal szemben- még csak azt
mondtuk meg, hogy a 'sampleWeight' egy valós tömb lesz,
de még nem hoztuk azt létre.
sampleWeight
= new float[23];
sampleWeight[0]
= 11.1;
sampleWeight[1]
= sampleWeight[5] = 2.4;
sampleWeight[2]
= (sampleWeight[0] + sampleWeight[5]) / 2.0f;
isValid
= (sampleWeight[2] > 1.0f) && !(sampleWeigth[0] <=
100.0f);
Ebből
látjuk, hogy új tömböt (meg majd új bármit is) létrehozni a
'new típusnév' formában tudunk, a tömb-elemekre pedig a szokott
formában hivatkozhatunk.
Az
értékadást eszerint a sima '=' jelöli, az aritmetikai operátorok
között a szokásos +, -, *, / (egész osztás), % (maradék)
működik, az összehasonlító operátororok == (egyenlő-e), !=
(nem egyenlő-e), <, >, <=, >=, a logikai operátorok !
(tagadás), && (és), || (vagy), és a zárójelezéssel
lehet a precedenciát felülbírálni.
Működik
a C-beli láncolt értékadás, azaz a 'v1 = v2' értékadásnak mint
kifejezésnek is van értéke, nevezetesen a v2.
A
bit-szintű műveletek szintén a C szerintiek: & (bináris és),
| (bináris vagy), ^ (bináris kizáró vagy), ~ (bináris
invertálás), << (balra shift), >> (jobbra shift),
illetve egy újdonság: >>> (előjel nélküli jobbra
shift).
Működik
egyébként a C-ből ismerős prefix és postfix növelés és
csökkentés:
'változó++':
növeld a változót eggyel, a kifejezés értéke a növelés
előtti
érték
'++változó':
növeld a változót eggyel, a kifejezés értéke a növelés utáni
érték
'változó--':
csökkentsd a változót eggyel, a kifejezés értéke a csökkentés
előtti
érték
'--változó':
csökkentsd a változót eggyel, a kifejezés értéke a csökkentés
utáni
érték
Hasonlóképpen
működnek a műveletes értékadások:
'v1
+= v2': v1 = v1 + v2
'v1
-= v2': v1 = v1 - v2
stb.
És
az egyetlen három operandusú művelet:
Egyébiránt
a 'new' is operátor: a 'new típusnév' értéke egy, az adott
típusból létrehozott új példány :D. Lesz még 4 db operátor
(metódushívás, attribútum-elérés, típus-ellenőrzés és
típus-konverzió), azokról viszont később esik majd szó.
Blokkok
A C-ből ismert módon
a { }-ek közötti rész egységnek számít, azaz mindenhol állhat,
ahol egyedi utasítás állhat, és a tartalma szépen
szekvenciálisan hajtódik végre (kivéve...)
Az egyetlen említésre
méltó dolog az, hogy a blokkokban bárhol szintén lehet változókat
deklarálni, amik a blokkból kilépéskor automatikusan
megsemmisülnek. Ha egy blokkbeli változónak ugyanaz a neve, mint
egy blokkon kívülinek, akkor a belső 'elfedi' a külsőt.
Vezérlési
struktúrák
Egy az egyben a C
mintájára zajlik:
Feltételes
végrehajtás
if
(logikai_kifejezés)
ha_igaz_akkor_teendő;
if
(logikai_kifejezés)
ha_igaz_akkor_teendő;
else
különben_teendő;
Érték
szerinti elágazás
switch
(egész_vagy_enum_kifejezés) {
case
érték1:
teendő1;
break;
case
érték2:
teendő2; // szándékos fallthrough
default:
különben_teendő;
}
Feltétel-vezérelt
ciklusok
while
(logikai_kifejezés)
amíg_igaz_teendő;
do
{
amíg_igaz_de_legalább_egyszer_teendő
} while
(logikai_kifejezés);
C-szerű ciklus
for
(kezdeti_művelet; bennmaradási_feltétel; léptetés)
teendő;
for
(int i = 1; i < 2048; i = 2*i)
System.out.println(i);
Iteráló ciklus
for
(változó: tömb_vagy_collection)
teendő;
for
(String s: args)
System.out.println("arg: " + s);
Visszatérés
függvényből/metódusból
return;
// void metódusból
return
visszatérési_érték; // kifejezés-jellegű metódusból
Kilépés for/while ciklusból
while
(...) {
...
break;
...
}
kulso_cimke:
while (...) {
while
(....) {
...
break
kulso_cimke;
...
}
}
For/while ciklus
folytatása
while
(...) {
...
continue;
...
}
kulso_cimke:
while (...) {
while
(....) {
...
continue
kulso_cimke;
...
}
}
Kivétel-kezelés
Technikailag ez is ide
tartozna, de logikailag sokkal később, mert kell hozzá pár egyéb
koncepció ismerete.
Osztályok és
objektumok
A tömböt már
néztük, a másik nevezetes típus-konstrukciót, a struktúrát itt
osztálynak hívják, mert pusztán struktúra a Javában nincsen.
class
Person
{
String
givenName, familyName;
int
height, weight;
}
Ezzel definiáltunk
egy új összetett típust, amiből példányokat szintén a
'new'-val hozhatunk létre:
Person
jane, john;
jane
= new Person();
jane.givenName
= "Jane";
jane.height
= 165;
john
= new Person();
john.givenName
= "John";
john.height
= 176;
Mint láthatjuk, ez a
'new' egy kicsit másabb a tömbös new-nál, itt () zárójelpár
van mögötte.
Hasonlóképpen az is
kiderült, hogy az objektumpéldányok attribútumaira a
'változó.attribútum' formában hivatkozhatunk - ez a '.' a már
említett 4 maradék operátor egyike :D.
Komoly probléma
persze, hogy így az objektum adatainak a kezelését, beleértve az
inicializálást is, a hívóra bíztuk, ami nem túl szerencsés.
Az osztály és a
struktúra egyik leglényegesebb különbsége, hogy a struktúra
csak összetartozó adatokat fog egybe, az osztály viszont az ezeket
kezelő kódot is tartalmazza.
Például amíg egy
String példány (ami maga is egy objektum) nincs inicializálva,
addig egy speciális értéket a 'null'-t tartalmaz, és bármit
próbálnánk kezdeni vele, az futási hibát eredményezne. Ha
viszont a hívóra bízzuk az adatmezők kezelését, akkor semmi
garancia, hogy ez nem következik be, például a fenti kódban a
példányok 'givenName' mezői kaptak értéket, de a 'familyName'-ek
nem.
Ezt azzal tudjuk
megelőzni, ha megadjuk azt a kódot, amivel ezen típus egy-egy új
példányát inicializálni kell: a konstruktort.
class
Person
{
String
givenName, familyName;
int
height, weight;
public
Person() {
givenName
= familyName = "N/A";
height
= weight = 0;
}
}
Így amikor valaki
leírja, hogy 'x = new Person()', akkor a fenti Person.Person() fog
lefutni, és az korrektül inicializál mindent.
Mint láthatjuk, az
osztály attribútumaira a saját metódusaiban csak szimplán az
attribútumnévvel hivatkozhatunk, ez az
épp aktuális példány
attribútumát jelenti.
Magára az aktuális
példányra a 'this'
kulcsszóval hivatkozhatunk, tehát pl. írhattuk volna azt is, hogy
'this.givenName = ...'.
Persze nem csak az
inicializálás az egyetlen művelet, amit megadhatunk:
class
Person
{
//
...
public
String toString() {
return
givenName + " " + familyName + " (" + height + "
cm, " + weight + " kg)";
}
//
...
}
//
...
System.out.println(jane.toString());
System.out.println(john);
A fenti 'toString()'
egy kicsit mágikus név, abban az értelemben, hogy ha valahová
String-re lenne szükség (mint pl. a System.out.println(String)
kiírásban), de egy objektum-példányt adtunk meg, akkor az
implicite konvertálódik a toString() metódusának segítségével.
Ez kicsit hasonlít
ahhoz a mágiához, ahogy Stringeket a '+' jellel össze tudunk
fűzni, ez szintén 'syntactic sugar', elvileg ez nem volna helyes
kifejezés.
(Természetesen a
'System' is valójában 'java.lang.System', ami ugye szintén egy
mágia :D
Sokan felróják az
ilyesmit a Java nyelvnek, nem is teljesen alaptalanul...)
Objektumok
megszűnése
Konstruktort már
írtunk, mi a helyzet az ellenpárjával, a destruktorral? Olyan
nincsen!
A Javában minden
objektum-példány implicite tartalmaz egy referencia-számlálót,
ami nő eggyel, amikor a példány címét
egy új változónak értékül adjuk, és csökken, amikor az a
változó egy új értéket kap. Például a fenti példában a két
Person-példány refcountja 1-1, mert mindegyikre 1 változó (a jane
és a john) hivatkozik.
De ha ezt írnánk:
//
...
Person
x = jane;
//
a 'jane' által hivatkozott példány refcountja itt már 2
Person
y = john
//
most már a 'john' által hivatkozotté is
x
= y;
//
a 'jane' által hivatkozotté most megint 1, a 'john'-é pedig 3
jane
= null;
//
a korábban a 'jane' által hivatkozott példány refcountja már 0!
Azt hiszem, érthető
a dolog :D.
Amikor
elvesztettük/eldobtuk az utolsó hivatkozást is egy példányra,
akkor annak a refcountja 0-ra csökken, és már ha akarnánk, se
tudunk többé rá hivatkozni, így ez fölöslegessé
vált.
Ilyenkor ez a példány
a 'szemétkosárba' kerül, és majd valamikor, amikor a JVM épp
ráér, akkor szép komótosan elkezdi őket felszabadítgatni.
Például a most megszüntetett Person példánybeli két
String-példány refcountjait csökkenti, és mivel azok is 0-ra
futottak, szintén mennek a szemétbe.
Persze az élet kissé
bonyolultabb, pl. ha a Person-t kiegészítjük így:
class
Person
{
//...
Person
spouse;
}
//
...
Person
jane = new Person(...);
Person
john = new Person(...);
//
mindkét példány refcountja 1-1
jane.spouse
= john;
//
a 'john' példány refcountja 2, mert ennyien hivatkoznak rá: john
és jane.spouse
john.spouse
= jane;
//
most már a 'jane' példány refcountja is 2
john
= jane = null;
//
mindkét példány refcountja 1-1 (egykori_john.spouse és
egykori_jane.spouse)
Akkor most ezek a
végtelenségig életben tartják egymást, vagy mégsem?
A felszabadító
mechanizmus elég okos ahhoz, hogy az ilyen köröket felismerje,
azaz valójában amihez a
főprogramból nem lehet eljutni,
azt szabadítja fel, a refcount csak könnyítheti a döntést.
Szóval ha egy
példányra már nincs szükségünk, akkor csak nullázzunk ki
minden rá mutató referenciát, és a többit a Garbage Collector
(GC) majd elintézi.
Paraméterek a
metódusoknak és polimorfizmus
Természetesen a
hívási paraméterek fogalma itt is megvan, sőt, arra is van
módunk, hogy ugyanazon nevű metódust más és más
paraméterlistával újra definiáljuk:
//
...
public
Person(String gn) {
givenName
= gn;
familyName
= "N/A";
height
= weight = 0;
}
public
Person(String givenName, String familyName) {
this.givenName
= givenName;
this.familyName
= familyName;
height
= weight = 0;
}
public
Person(Person other) {
givenName
= other.givenName;
familyName
= other.familyName;
height
= other.height;
weight
= other.weight;
}
//...
Person
jane = new Person("Jane");
Person
john = new Person("John", "Doe");
Person
john2 = new Person(john);
A híváskori
paraméterlista egyértelműen azonosítja, hogy melyik esetben
melyik konstruktort kell hívni. Természetesen ugyanez bármely
metódussal ugyanígy működik.
Szokásos trükk, hogy
ha valamelyik paraméter és valamelyik attribútum neve ütközik,
akkor az önmgában álló név a paramétert jelenti, ott az
attribútumra pedig a 'this.valami' formában hivatkozhatunk.
A harmadik
konstruktor, ami ugyanezen típus másik példányáról inicializál,
az nevezetes, és copy-constructornak
hívják, ahogyan a paraméter nélküli konstruktor neve default
constructor.
Öröklődés
"Minden bogár
rovar, de nem minden rovar bogár" - ismerős?
A példánknál
maradva, minden sofőr ember, de nem minden ember sofőr:
class
Driver extends Person
{
String
licenseNumber;
public
String toString() {
return
super.toString() + ", lic=" + licenseNumber;
}
public
Driver(String gn, String sn, String l) {
super(gn,
sn);
licenseNumber
= l;
}
public
string getLicenseNumber() { return licenseNumber; }
}
//
...
Driver john = new Driver("John", "Doe",
"12341234");
Person
johnp = john;
john.height
= 176;
System.out.println("john="
+ john);
System.out.println("johnp="
+ johnp);
Ennek az eredménye:
C:\Temp\dev_java>java
PersonTest
john=John
Doe (176 cm, 0 kg), lic=12341234
johnp=John
Doe (176 cm, 0 kg), lic=12341234
Tehát a sofőr is
ember, csak neki van jogosítványszáma is, itt Person-t
ősosztálynak, a Driver-t leszármazott osztálynak hívjuk, és az
erre vonatkozó szintaxis a 'class leszármazott extends ős'.
A Driver-példány
inicializálásához három string kell, az első kettő az ősosztály
konstruktorához használódik ('super(gn, sn)'), és ehhez nagyon
hasonlóan a 'toString()' is felhasználja az ősosztály
'toString()'-jét, meglehetősen hasonló jelölésmóddal.
A 'Person johnp =
john' sikeressége mutatja, hogy a Driver egyben Person is, ugyanis
az ellenkező irány, pl. egy 'john = johnp' hibaüzenetet adna:
error:
incompatible types: Person cannot be converted to Driver
john
= johnp;
És egyúttal azt az
amúgy fontos
dolgot is láthattuk, hogy a metódusok konkrét példányokhoz
kötődnek, tehát a System.out.println("johnp="
+ johnp)-nél hiába
hogy Person típusú johnp-ként hivatkoztunk a Driver-példányunkra,
akkor is a Driver.toString() hívódott meg.
Ezt hívják virtuális
metódusnak, C++-ban ezt
külön kéne jelölni, de Javában
minden metódus virtuális.
Szintén fontos, hogy
örökölni csak egyetlen
típusból lehet,
mégpedig azért, hogy megelőzzünk egy ütközést: ha lehetne pl.
két ős, és az egyik ős definiál egy 'x' attribútumot
'int'-ként, a másik ős meg 'String'-ként, akkor a leszármazottban
milyen típussal szerepeljen, és mi légyen a másik ősnek az ezt
piszkáló metódusaival? Nincs jó válasz, tehát nincs
többes öröklődés.
Megjegyzendő
még, hogy van egy 'Object' nevű típus, ami minden osztály-típusnak
implicit őse, azaz mindenki belőle származik, anélkül, hogy ezt
explicite kiírná. (Megint egy mágia.)
Az öröklődés jelentése
Az
öröklődés konkrétan új információval való bővülést
jelent, sem többet, sem kevesebbet.
A
valós életben ez helyzettől függően jelenthet akár
specializációt, akár generalizációt, úgyhogy a 'mit
származtassunk miből' tervezési kérdésnél az 'információval
bővülést' kéretik nézni!
Öröklődés mint specializáció
Az
Ember típus tartalmaz nevet, életkort, testsúlyt, magasságot, a
Sofőr típust pedig az Ember típusból származtathatjuk (a
'jogosítvány száma' lesz a bővülő információ).
Jelentését
tekintve a Sofőr pedig az Ember specializációja, mert minden Sofőr
egyben Ember is, de nem minden Ember Sofőr.
Öröklődés mint generalizáció
A
Négyzet típus tartalmaz oldalhosszot és színt, a Téglalap típust
pedig a Négyzet típusból származtathatjuk (a 'másik oldal
hossza' lesz a bővülő információ).
Jelentését
tekintve a Téglalap pedig a Négyzet generalizációja, mert bár
nem minden Téglalap Négyzet, de minden Négyzet egyben Téglalap
is.
Típus-ellenőrzés
és -konverzió
A fenti példában
'johnp' egy Person-referencia, tehát neki nem tudnánk pl. a
licenseNumber-ére hivatkozni, mert a Person-nak olyanja nincsen.
Megtehetnénk, hogy áterőltetjük Driver-ré, de ez
katasztrófához vezet, ha esetleg mégis csak egy sima Person-ra
hivatkozott, ami nem Driver. Tehát először ellenőrizni kellene:
if
((johnp != null) && (johnp instanceof Driver)) {
Driver
johnd = (Driver)johnp;
System.out.println("lic:
" + johnd.licenseNumber);
}
Mint láthatjuk, a
'változó instanceof típus' kifejezést kerestük az ellenőrzés
céljára, és a '(típus)változó'-t a típus áterőltetésére.
Absztrakt
osztályok
Ha egy
ős-osztály valamely metódusát nem valósítjuk meg (mert pl. nem
lehet értelmesen), de a leszármazottainak elő akarjuk írni, hogy
nekik felül kelljen
definiálni, akkor az ilyen osztályt hívjuk absztraktnak:
abstract
class Shape
{
abstract
public double getArea();
}
class
Circle extends Shape
{
public
double getArea() { return r*r*Math.PI; }
}
Természetesen
absztrakt osztályt nem lehet példányosítani, és ha származtatunk
belőle, akkor abban vagy minden absztrakt metódust meg kell
valósítani, vagy a leszármazott osztálynak is absztraktnak kell
lennie.
Láthatóság
A fenti példákban
pár helyen előfordult a 'public' szó, és ezt még nem veséztük
ki.
Bevezethetünk mi
olyan konstruktorokat, amilyeneket csak akarunk, ha a hívónak
megengedjük, hogy a példányaink attribútumait kedvére
elpiszkálja, pl. 'john.givenName = null'. Ezt csak úgy tudnánk
kizárni, ha a védendő atttribútumainkat csak
nekünk lenne
jogunk állítgatni.
class
Person
{
private
String givenName, familyName;
//
...
Ezután
az adott attribútumot csak az osztály saját metódusai tudják
elérni, más senki sem.
Ha
egy metódust teszünk 'private'-té,
akkor értelemszerűen csak az osztály többi metódusa fogja tudni
meghívni.
Ehhez
hasonló minősítő a 'public',
ami mindenkinek elérhetővé teszi az adott attribútumot/metódust,
illetve a 'protected',
ami csak az osztálynak és a leszármazottainak.
Mi
a helyzet azzal, ami se private, se protected, se public? Az az
úgynevezett 'package'
láthatóság,
amikor a velünk egy osztálykönyvtárbeli típusok metódusai
érhetik el, más nem.
Statikus
metódusok és attribútumok
Ha
valami olyan attribútumot vagy metódust szeretnénk létrehozni,
ami nem az egyes objektumpéldányokhoz,
hanem magához az osztálytípushoz
kötődik, arra a 'static'
kulcsszó alkalmas.
Ilyenek
pl. a konstansok, vagy ha pl. a létrehozott példányokat szeretnénk
megszámlálni:
class
HtmlColour
{
public
static int RED = 0xff0000;
public
static int GREEN = 0x00ff00;
public
static int BLUE = 0x0000ff;
private
static int numInstances = 0;
public
static int getNumInstances() { return numInstances; }
public
HtmlColour() {
numInstances++;
//
...
}
}
Ezekre
példányosítás nélkül is hivatkozhatunk:
int
x = HtmlColour.RED;
int
y = HtmlColour.getNumInstances();
(Igazából
a konstansok deklarációjában szokott még lenni egy 'final'
kulcsszó, pl. 'public static final int RED = 0xff0000;', ami annyit
tesz, hogy az adott változó/attribútum csak egyetlen egyszer
kaphat értéket, onnantól read-only, és így a Java fordító
ezzel tud optimalizálni.)
Egy kapcsolódó trükk:
Ha nem szeretnénk, hogy bárki tudja az osztályunkat példányosítani (mert pl. csak egyetlen példánynak van értelme belőle), akkor a konstruktort kívülről meghívhatatlanná tehetjük:
private Driver(...) {
// ...
}
static Driver theDriver;
public static Driver get() {
if (theDriver == null)
theDriver = new Driver(...);
return theDriver;
}
// ...
Driver x = Driver.get();
Ezt a kód-struktúrát hívja az irodalom 'Singleton pattern'-nek.
Interface-ek
Mint
láthattuk, Javában nincs többszörös öröklődés, és jó okkal
nincsen. Viszont helyenként igencsak praktikus volna, ha egy
általános célú kódban csak valamilyen képességet szeretnénk
feltételezni a kapott paraméterről, pl.:
abstract
class Readable {
abstract
public char read(); // read one character
}
String
readLineFrom(Readable x) {
String
s = "";
while
(true) {
char
c = x.read();
if
(c == '\n')
return
s;
s
= s + c;
}
}
Ez
egy sorvégjellel lezárt sorozatot olvasna be egy Stringbe bármiből,
ami a Readable-ből származik és megvalósította az
egy-karakter-olvasása műveletet. És ez így még működne is!
A
gond akkor jön, ha van hasonló igény pl. egy 'Writable' típusra,
és mi valami olyasmit szeretnénk megvalósítani, amit írni és
olvasni is lehet (pl. file, hálózati kapcsolat, soros terminál,
stb.):
class
Terminal extends Readable, Writable { ... }
Terminal
t;
String
s = readLineFrom(t);
sayHelloTo(t);
Hát
ilyen ugye nincsen, pedig az eset életszerű. Erre szolgál a
Javának az interface
fogalma:
Az
interface valamelyest hasonlít az absztrakt osztályokra, csak
Az
interface-nek nem lehetnek attribútumai, csak metódusai
Az
összes metódus implicite public, ezt nem kell külön jelölni
A
metódusokat nem kell külön absztraktnak sem jelölni, elég, ha
nincsen törzsük
Az
interface tartalmazhat statikus metódusokat és konstansokat is
Az
interface-ből nem leszármazik valaki, hanem megvalósítja
azt,értve
ez alatt azt, hogy megvalósít minden, az interface-ben megadott
metódust
Az
interface-ben a metódusokhoz a 'default' kulcsszóval adhatunk meg
alapértelmezett törzset, ami csak akkor lép érvényre, ha az
interface-t megvalósító osztály azt nem valósítaná meg (pl.
mert az interface bővült az osztály írása óta)
Interface-ek
örökölhetnek egymástól
Ahol
típus állhat, ott interface is, és oda illeszkedik bármilyen
típus, ami megvalósítja az adott interface-t.
Tehát
pl.:
interface
Readable
{
char
read();
}
interface
Writable
{
bool
write(char c);
}
class
Terminal implements Readable, Writable
{
public
char read() { ... } // from Readable
public
bool write(char c) { ... } // from Writable
//
...
Generikus
típusok
Képzeljünk
el egy verem-implementációt, amibe valamilyen típusú objektumokat
pakolhatunk be, illetve ugyanezeket vehetjük ki onnak, lesz pl. egy
'void push(...)' meg egy 'Valami pop()' metódusa. No de mi legyen ez
a 'Valami'?
Ha
én embereket akarok tárolni, akkor Person, ha terminálokat, akkor
viszont Terminal. Lehetne a végső közös ős, az Object, de akkor
a kivett elemekkel nem könnyen lehetne bármit is kezdeni, amit az
Object típus nem tud:
Object
x = verem.pop();
x.write('Q');
// hiba: write()-ja pl. a Terminal-nak van, de az Object-nek nincs
Valahogy
a listának meg kéne kapnia a típust is, amilyen objektumokat
fogadnia és visszaadnia kell:
interface
List<T>
{
void
push(T x);
T
pop();
}
List<Person>
emberek = new List<Person>();
List<Terminal>
terminalok = new List<Terminal>();
emberek.push(john);
Person
x = emberek.pop();
A
fenti példában a 'T' egy típus vagy interface helyén állt, és a
kiértékeléskor az épp aktuális típussal helyettesítődött be.
Természetesen
ez nem csak egybetűs lehet, és nem is csak egy lehet belőle, pl. a
kulcs-érték párokat tartalmazó java.util.Map szignatúrája
'public interface Map<K, V>', ahol K és V a kulcs és az érték
típusai.
Csak
konvenció, de pár, egy db nagybetűből álló nevet bizonyos fajta
típusok jelölésére használunk:
Felsorolási
konstans típusok
Ha
pl. színeket akarunk megadni, akkor az eddigiek alapján egy típust
vezetnénk be, és abban definiálnánk konstansokat (lásd pl.
HtmlColour fentebb).
Van viszont erre egy cizelláltabb, erősebb
nyelvi lehetőség, az 'enum':
public
enum Planet {
MERCURY
(3.303e+23, 2.4397e6),
VENUS
(4.869e+24, 6.0518e6),
EARTH
(5.976e+24, 6.37814e6),
MARS
(6.421e+23, 3.3972e6),
JUPITER
(1.9e+27, 7.1492e7),
SATURN
(5.688e+26, 6.0268e7),
URANUS
(8.686e+25, 2.5559e7),
NEPTUNE
(1.024e+26, 2.4746e7);
private
final double mass; // in kilograms
private
final double radius; // in meters
Planet(double
mass, double radius) {
this.mass
= mass;
this.radius
= radius;
}
private
double mass() { return mass; }
private
double radius() { return radius; }
//
...
Mint
láthatjuk, egészen class-ként viselkedik, csak meg lehet adni a
lehetséges értékek listáját is.
Package-ek
Láttuk,
hogy mindenféle osztálykönyvtárak szép kompakt csomagokban
jönnek, és nem ezernyi külön .class file-ban, hát ilyet mi is
tudunk csinálni!
Válasszunk
egy package nevet, pl. org.dyndns.fules.demo, és hozzunk létre egy
ennek megfelelő könyvtárstruktúrát: org/dyndns/fules/demo.
Az
egyes osztályainkat (Person, Driver, PersonTest) válogassuk
külön-külön file-okba ezen könyvtárstruktúrában (Person.java,
Driver.java, PersonTest.java).
Mindegyik
ilyen file elején adjuk meg a package nevét, amihez tartozik:
package
org.dyndns.fules.demo;
Forgassuk
le őket és futtassuk a PersonTest-et:
javac
org\dyndns\fules\demo\*.java
java
org.dyndns.fules.demo.PersonTest
A
.class file-ok a .java-k mellé keletkeztek, tehát még csak félúton
vagyunk.
Ezt
kissé póriasan, de már összefűzhetjük egy archive-ba, és el is
lehet indítani:
jar
cvf valami.jar org\dyndns\fules\demo\*.class
java
-cp valami.jar org.dyndns.fules.demo.PersonTest
Bár
ekkor még kézzel kellett megadni ClassPath-ként (-cp) a .jar-t, és
az indítandó osztály nevét, amit be is lehetne ágyazni a
.jar-ba:
jar
cvfe valami.jar org.dyndns.fules.demo.PersonTest
org\dyndns\fules\demo\*.class
java
-jar valami.jar
Itt
az 'indítandó osztály' annyit jelent, hogy amikor a JVM indul,
akkor ennek az osztálynak keresi a 'public static void main(String[]
args)' metódusát, amit meghív, paraméterként átadva a kapott
parancssori argumentumokat.
Ha
kívülről, más osztályokból szeretnénk ezt a csomagot
használni, akkor
Egyrészt
azok fordításánál a '-cp valami.jar' -ral a java .class-ok
keresési útvonalához hozzá kell adni
Másrészt
a használó .java file elején a kívánt osztályt be kell
importálni, pl. 'import org.dyndns.fules.demo.Driver;'
Az
import-nak van még két fajtája:
On-demand:
'import org.dyndns.fules.demo.*;' ekkor minden közvetlen ottani
típus beinclude-olódik, amit használni akarunk
Static
import: 'import static HtmlColours;' ami után a csomagbeli
attribútumok is direkt elérhetőek lesznek, azaz nem kell
'HtmlColours.RED', hanem elég csak a 'RED'
Beágyazott
osztályok
Eddig
minden osztály top-level volt, azaz a package-én belül legkívül,
de persze ezt lehet cifrázni is, méghozzá négyféleképpen:
Inner
class
Ez
az a helyzet, amikor egy osztályon belül definiálunk egy másikat,
de az nem static.
Ekkor
a belső osztályra csak a külső példányai tudnak hivatkozni,
tehát a belső minden példányához tartozik a külsőnek egy
példánya, ami létrehozta.
Ezért
semmi akadálya, hogy a belső osztály elérje a külsőnek ezen
példányát!
class
Genus
{
String
genusName; // pl. 'canis'
class
Species {
String
speciesName; // pl. 'aureus'
class
Subspecies {
String
subspecName; // pl. 'indicus'
public
String toString() {
return
genusName + " " + speciesName + " " +
subspecName;
}
}
Subspecies[]
subspecies;
}
Species[]
species;
}
//
...
Genus[]
genus;
//
...
System.out.println(genus[4].species[1].subspecies[3].toString());
Ha,
tegyük fel, mindhárom class csak simán 'name'-nek hívta volna az
attribútumát, az sem lenne baj, csak ekkor a toString() így
alakulna:
public
String toString() {
return
Genus.name + " " + Species.name + " " + name;
}
Az
Inner classok természetesen nem tudnak statikus dolgokat deklarálni,
merthogy ők mindig a külső osztály valamely példányához
kötődnek, a statikus dolog meg attól független volna.
Mivel
az interface-ek eredendően példány-függetlenek, ezért Inner
interface nincsen :D.
Static
nested class
Ha
a belső osztály 'static', akkor értelemszerűen nem tud a külső
osztálynak a példányhoz kötődő, tehát nem-static dolgaihoz
hozzáférni.
Igazából
ez olyan, mintha maga is top-level class volna, csak
kényelmi/csoportosítási okból került volna egy másik belsejébe.
Erre
még kívülről is lehet hivatkozni 'x = new Külső.StaticBelső(...)'
formában.
Local
class
Az
Inner class unokatesója, csak ezt egy tetszőleges { } blokkon belül
definiáljuk és használjuk. Egyébiránt ugyanaz pepitában.
Anonymous
class
Amikor
csak azért kellene egy új classt definiálni, hogy egyetlen helyen
egyetlen példányt létrehozzunk belőle, akkor ez megspórolható:
Thread
t = new Thread(new Runnable {
public
void run() {
//
csinál valamit egy külön threaden
}
}
);
t.start();
Itt
mi tulajdonképpen a Runnable interface-t valósítottuk meg egy
névtelen osztállyal, definiálva annak a run() metódusát, ebből
hoztunk létre egy új példányt, amit átadtunk egy Thread
konstruktorának, majd elindítottuk ezt a threadet.
Exception
handling
A
Java hibakezelése nem feltétlenül korlátozódik a
hibakód-visszadásra, hanem van lehetőség egy kissé rugalmasabb,
bár nagyobb odafigyelést igénylő módszerre is.
Amikor
valahol a hívási lánc mélyén valami olyasmi történik, ami a
további végrehajtást lehetetlenné teszi (pl. hálózati hiba
lépett fel), akkor a hiba helyénél mód van a végrehajtást
azonnal megtörni, az egymásba ágyazódó blokkokból azonnal
kilépkedni (kb. mint ciklusból a break), egészen addig, amíg
valaki fel nincs készülve ennek az eseménynek a kezelésére.
Hogy
milyen esemény is történt, illetve annak milyen részletei voltak,
azt úgy tudjuk feljuttatni ehhez a kezelőhöz, hogy tulajdonképpen
egy objektum-példányt 'dobunk' felfelé neki:
throw
new NetworkErrorException("füstöl a drót");
Aki
pedig ezt el akarja kapni (tetszőleges szinttel feljebb), az így
teheti meg:
try
{
//
csinál valamit, amiből jöhet a NetworkErrorException
}
catch
(NetworkErrorException e) {
//
lekezeli az eseményt
}
catch
(FileNotFoundException e) {
//
lekezel egy másik eseményt is
}
finally
{
//
ez még akkor is lefut, ha netán valami egyéb exception jött, amit
mi is engedünk tovább
//
kiváló hely a cleanup teendőkre
}
Ha
egy metódus dobni (vagy továbbengedni) szeretne egyfajta
exceptiont, akkor ezt a metódus deklarációjában jelezni kell:
public
void Valami() throws NetworkErrorException, IOException { ...
És
még egy Jáva-mágia: kivéve,
ha az illető exception a RuntimeException-ből származik, mert azt
nem kell külön jelezni...
A
primitív típusok wrapperei
Ezzel
még adósok vagyunk, bár igazából sehová sem passzol rendesen
:D.
A
primitív típusokat lehet használni kifejezésekben, és ennyi. Nem
tudják megmondani az értékkészletüket, nem tudják magukat
Stringgé konvertálni (tényleg nem! az mágia volt!), sem Stringből
kielemezni, stb., mert nem
objektumok.
Hasonlóképpen
nem lehet őket generikus típusokban sem használni, tehát nincs
'List<byte>'.
Ezt
feloldandó mindegyik kapott egy wrapper classt, ami mindezeket tudja
helyette:
Byte,
Integer, Long, Boolean, Character, Float, Double.
Nekik
van mindenféle szépségük, konstruktor a primitív típusról ill.
Stringről, toString(), static valueOf(String s), stb.
Miért
van hát akkor szükség a primitív típusokra?
Mert
a Javában nincs operator overloading, azaz objektumokat nem tudunk
aritmetikai/logikai kifejezésekben szerepeltetni, hozzájuk pl. '+'
operátort definiálni.
Ezért
kellett, hogy ezek nyelvi elemek legyenek.
És
mi is volt az a Stringesítő mágia?
Amikor
azt írjuk, hogy
int
i = 42;
System.out.println("i
értéke " + i);
akkor
valójában az int-ről implicite keletkezik egy Integer, és annak
hívódik a toString()-je.
Mondom
hogy mágia :D !
Static
initializer blokkok
Ha
valahol, bárhol lerakunk ilyen blokkokat:
static
{
//
bármi kód
}
akkor
ezek le fognak futni, amikor az adott class betöltődik, méghozzá
fentről lefelé sorrendben és pontosan egyszer.
Ronda
mágiákat lehet ezzel csinálni, no pláne az enum-ok
inicializálásával átfűzve... Ha nem muszáj, ne tegyük!
A
leggyakrabban használatos standard package-ek és osztályok
A
teljesség igénye nélkül, csak az említés szintjén:
java.lang
Object,
String, Math,
Byte, Integer, stb.,
java.util
List<T>,
Deque<T>, Map<K,V>, Vector<T>, Locale
java.text
Format,
SimpleDateFormat
java.io
Stream,
Reader, Writer, File
javax.swing
GUI
komponensek
Mivel
ezek már szigorúan véve nem a nyelv elemei, így ezekről egy
külön jegyzetben esik majd szó.