A Java alkonya?

Fő kategória: Java.

Egyre több jel mutat arra, hogy a Java hanyatlásnak indult. Attól nem kell persze tartanunk, hogy egyik napról a másikra teljesen eltűnik, hiszen nagyon sok kód íródott Java-ban, de a tendencia egyértelműnek látszik. Ezen az oldalon felsorolok néhány olyan szempontot, ami szerintem hozzájárult ahhoz, hogy lassan de biztosan a Java is a programozási nyelvek süllyesztőjébe kerüljön.

Betanulási görbe

A Java betanulási görbéje már-már szinte vállalhatatlanul hosszú.

Az első lépések megtétele is komoly próbatétel! Tegyük fel, hogy sikeresen feltelepítettük a Java-t (erről még lesz szó részletesebben), már az első, Hello, world! program is eléggé komplikált:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

Ezt még le kell fordítani (erről is lesz szó a későbbiekben):

javac HelloWorld.java

Majd futtatni:

java HelloWorld

A fentiek elmagyarázása rengeteg időt vesz igénybe.

Érdemes összehasonlítani a Pythonnal:

print('Hello, world!')

Vagy a JavaScripttel. Ez utóbbihoz indítsuk el a böngészőben (pl. itt rögtön) a Developer Tools-t (általában F12), nyissuk meg a konzolt (Console; általában ez az alapértelmezett), majd írjuk be ott ezt:

console.log('Hello, world!')

Ez utóbbiaknál nincs mit magyarázni, és sokkal gyorsabban jutunk sikerélményhez, mint Java-ban.

De ahhoz, hogy valakinek Java fejlesztőként legalább elvi esélye legyen elhelyezkedni akár csak egy junior pozícióban is, a következőket kell megtanulnia:

  • A Java nyelv, ami önmagában nem egyszerű.
  • A leggyakoribb belső és külső könyvtárak, amelyek jóval bonyolultabbak, mint pl. a Python társai.
  • Az Enterprise Java, amely terület - ha lehet - még sokkal nagyobb, bonyolultabb, összetettebb, mint a Standard Java.

És mindez csak egy belépő szint; a specializálódás elengedhetetlen. Olyan szinte nincs, hogy valaki most már mindent tud, és tanulás nélkül tudja megfoldani a feladatokat. Ez utóbbi persze más nyelvekben is igaz, viszont kevésbé szerteágazó. Pl. egy Python programozónak, ha adatfeldolgozással kezd foglalkozni, akkor meg kell tanulnia mondjuk a Pandas könyvtárat, de lényegében csak azt, és azzal úgymond "el lehet lenni". A Java nem ilyen.

Hangsúlyos hogyanok, hiányzó miértek

A Java egyes része annyira komplikáltak, és oly sokféle megoldás létezik, hogy gyakran "elvész a sok bába között a gyerek": a tanulásnál a probléma kerül a fókuszba, amire van egy megoldás, hanem a túlbonyolított megoldások tömkelege. Ezekről még lesz szó bővebben, most csak egy példán illusztrálom, hogy mit szeretnék ezzel állítani. Vegyük a fájlba írás ill. a fájlból olvasás példáját! Kismillió megoldás létezik, egyik bonyolultabb, mint a másik, gyakran ütközünk problémákba; nem mondhatjuk azt, hogy ez a feladat és erre ez a megoldás. Lehet így is, lehet úgy is, de milyen jó, hogy már lehet amúgy is. A Java-t tanuló könnyen elveszíti a fonalat; a szerteágazó lehetőségek részletkérdéseit már nem is érti, és ez frusztrálólag hat rá. A töménytelen információt - ami ráadásul nem is strukturáltan érkezik, hanem mondjuk egy tanfolyamon tanított tananyag mellékszála mellékszálaként - nagyon nehéz rendszerbe helyezni.

Ellenpéldák más programozási nyelvekben:

  • Fájl olvasás: általában van egy függvény a fájl megnyitására (open()), olvasásra (read()) és annak lezárására (close()). Java-ban van a kezdeti buffered akármilyen file stream, amit persze három lépésben kell átalakítani, majd ezt sokféleképpen egyszerűsítették és könnyítették, újabb és újabb osztályokat, függvényeket, paramétereket bevezetve, hogy a végén annyira fájdalmasan "könnyű" már a fájlkezelés, hogy az embernek már szinte az életkedve is elmegy tőle, nemcsak a Java programozásból ábrándul ki.
  • JSON konverzió: általában van két függvény, az egyik bekódol, a másik kikódol. Java-ban ehhez külön könyvtárak kellenek, de itt is bejön az "így is lehet" meg "úgy is lehet"; ilyen annotáció meg olyan nem tudom én micsoda; minimum nehézkes. És ez még csak a JSON; az XML még ennél is bonyolultabb.
  • Adatbázisból olvasás: többnyire van egy függvény, amivel kapcsolódunk az adatbázishoz (connect()), egy másikkal (query()) SQL parancsokat tudunk kiadni, amit sorról sorra be tudunk olvasni (fetch() egy cikluson belül), végül egy függvény, amivel lezárjuk a kapcsolatot (close()). Java-ban különböző könyvtárak kellenek, általános JDBC, adatbázis specifikus JDBC. A kapcsolaton túl még van Statement is, és az eredmény kiolvasása is nehézkes. Hasznos, bár nagyon sok időt vesz igénybe a megtanulása és sok a buktatója a Hibernate-nek, ami a kezdőket inkább csak összezavarja, mint segíti.
  • Webes szolgáltatások: lekérdezésnél általában van egy megfelelő függvény, pl. get() vagy post(), ami egy-két paraméterrel rendesen működik. Szerver oldalon általában létre kell hoznunk egy szervert megadjuk neki a portot, valamint egy olyan függvényt, ami adott kérésre adott választ adja. Java-ban néhány kapcsolódó fogalom csak felsorolás szinten, elrettentésül: van JAX-WS, RPC, JAX-RPC, WSDL, SOAP, UDDI, SAAJ, JAX-WS, JAX-RS. Ill. ezek megvalósításai. Mindegyiknél van az, hogy "lehet így", meg "lehet úgy is", de "amúgy aztán már tényleg nagyon könnyű"; hát nem az! Oktatási-tanulási szempontból: egy valóban könnyen tanulható programozási nyelven egy tanóra alatt át lehet venni a témát, Java-ban külön tanfolyamsorozatot kell indítani ehhez.

Hosszú kód

A Java kód sokszor iszonyatosan hosszú tud lenni, tele sallanggal. Lássunk egy példát! Tegyük fel, hogy egy függvény két dolgot szeretne visszaadni: egy szöveget és egy számot. Más nyelvekben ehhez kiválóan alkalmas a tuple, de ilyen a Java-ban nincs. Osztályt kell készítenünk. Csakhogy:

  • Az adatmezőket a konvenciók alapján nem illik publikussá tenni; azoknak privátnak kell lenniük, megfelelő getterekkel és setterekkel (ez utóbbira nem feltétlenül van szükség).
  • Kell egy konstruktor.
  • Gyakran meg kell valósítani az equals() metódust, ami automatikusan maga után vonzza a hashCode()-ot is.
  • A toString()-et sem kerüljük el.
  • És mindezt új fájlba.

Onnan indultunk ki, hogy egy elem kettesre van szükség, és máris itt tartunk:

public class MyStruct {
    private String myText;
    private int myNumber;
 
    public MyStruct(String myText, int myNumber) {
        this.myText = myText;
        this.myNumber = myNumber;
    }
 
    public String getMyText() {
        return myText;
    }
 
    public void setMyText(String myText) {
        this.myText = myText;
    }
 
    public int getMyNumber() {
        return myNumber;
    }
 
    public void setMyNumber(int myNumber) {
        this.myNumber = myNumber;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + myNumber;
        result = prime * result + ((myText == null) ? 0 : myText.hashCode());
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyStruct other = (MyStruct) obj;
        if (myNumber != other.myNumber)
            return false;
        if (myText == null) {
            if (other.myText != null)
                return false;
        } else if (!myText.equals(other.myText))
            return false;
        return true;
    }
 
    @Override
    public String toString() {
        return "MyStruct [myText=" + myText + ", myNumber=" + myNumber + "]";
    }
 
}

Ki se fér egy oldalra! Ennek többé-kevésbé megfelelő Scala kód:

case class MyStruct(myText: String, myNumber: int)

Valójában nagyon sok kód felesleges! Lássunk erre néhány példát:

  • A láthatóságot beállító módosítók (public, protected, private): lehetnének megfelelő alapértelmezett értékek, pl. az adatmezők privátok, a függvények publikusok, és az ettől eltérőt kellene csak beállítani.
  • A final. A funkcionális programozási nyelvekben ennek szerepe van, csakhogy a Java nem igazán funkcionális. Van értelme megkülönbözteti a változót a konstanstól, viszont a Java-ban alapértelmezésben minden változó, és a konstanst kell külön jelölni a final kulcsszóval. Persze mindenhova oda lehet írni ezt, ami nem változik, csakhogy ez inkább csak felesleges és idegesítő 6 plusz karakter. Összehasonlítva: a Scala-ban var és val, a JavaScriptben let és const stb.; egyenlő vagy összemérhető hosszú a jelölés.
  • Legalább a fő függvény lehetne kivételes. A public static void main(String[] args) a leghosszabb belépő fejlés, amivel valaha találkoztam.

És még számos egyéb, melyekre később látunk majd példát.

Verzió paralízis

A Java-nak nagyon sok verziója jött már ki, ami egyrészt örvendetes, másrészt a túl sok változásnak meg vannak a maguk hátulütői. Talán a legszembetűnőbb az, hogy a Maven alapértelmezésben 5-ös Java-t használ, és frusztráló, hogy mindig be kell írni pl. ezt:

<properties>
    <maven.compiler.target>1.8</maven.compiler.target>
    <maven.compiler.source>1.8</maven.compiler.source>
</properties>

Ami négy teljesen felesleges sor, ráadásul kettő közülük kifejezetten hosszú. És most melyiket írjuk? Az 1.8-at vagy a 8-at? (Mindkettő helyes.) Vagy azt, amit épp használunk? Érthetetlen módon maga az Eclipse is alapértelmezésben 5-ös Java-t használ; külön át kell állítani.

Számos esetben eltér a parancssori és az IDE-ben használt verzió, és elég nehézkes rendesen beállítani; általában ezek jól el vannak rejtve.

Átgondolatlanság

Egy-egy problémát kismillió módon meg lehet oldani, és gyakran egyik bonyolultabb, mint a másik. Emögött kitapintható egyfajta kapkodás, átgondolatlanság. Előfordul az is, hogy valamit bevezetnek egy verzióban, majd kiveszik a következőből, tehát még elvileg sem felülről kompatibilis, ld. pl. a value break szintaxist (a switch…case struktúrában pl. a break 5;).

Maven problémák

A Java-nak elvileg nincs köze a Mavenhez, viszont ez de facto szabványnak tekinthető, legalábbis általában nem megkerülhető. A Maven - ha lehet - még bőbeszédűbb, mint a Java. Lássunk egy minimalista konfigurációt:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>mytest</artifactId>
    <version>1.0</version>
</project>

Ezt azért egyetlen sorba is bele lehetne írni, ugyanis csak a group (hu.faragocsaba), a programnév (mytest) és a verzió (1.0) hordoz információt. A pom.xml szinte kötelező eleme még a fenti properties.

Gynege kezdés után erős a visszaesés: ha pl. az egy szem osztályból és egy szem függőségből álló programunkból egy futtatható jar-t szeretnénk készíteni, akkor a következő pom.xml ad mintát:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>guava-example</artifactId>
 
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <properties>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>
                                ${project.build.directory}/libs
                            </outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
 
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <classpathPrefix>libs/</classpathPrefix>
                            <mainClass>
                                hu.faragocsaba.main.Main
                            </mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Persze lehet ezt még tovább bonyolítani egyéb pluginokkal stb.

A Maven összehasonlíthatatlanul bonyolultabb, mint mondjuk a pip a Python-ban, az npm a NodeJS-ben (JavaScript), vagy akár az sbt a Scala-ban.

IDE problémák

Nekem az a tapasztalatom az IDE-kkel, hogy mindegyik tele van problémával, max. hozzá lehet szokni. Ennek is persze nem sok köze van a Java-hoz, de a fejlesztők bele-bele botlanak. Az írás pillanatában a két legnépszerűbb Java fejlesztőeszköz az Eclipse és az IntelliJ IDEA. Hosszasan lehetne sorolni a problémákat egyiknél és másiknál is. A kisebb, apró bosszúságot okozó hibákon mellett talán az egyik legjelentősebb probléma a Java verzióval kapcsolatos: hány és hány esetben fordult elő már az, hogy az IDE-ben nem működött a konzolról igen (vagy fordítva), és órák mentek el a probléma kiderítésével!

Nehézkes hibaüzenetek

A Java fejlesztők szinte kilométer hosszú stack trace-ekkel találkoznak, amiből sokszor elég nehéz kihámozni, hogy mi a probléma. Már egy átlagos Java program esetén is, ha egy hiba egy rendszer függvényhíváson belül van, könnyen belefutunk olyan hibába, ahol a stack trace ki se fér a képernyőre; az Enterprise Java-ban egy tipikus stack trace esetén többször is Page Down-t kell nyomni.

Sok esetben nehezen felderíthető apró beállítási probléma okoz galibát, és egy semmitmondó hibaüzenet hosszú stack trace-szel csak lassítja a felderítés folyamatát. Hány és hány alkalommal fordult elő, hogy a kollégámnál működött valami, nálam nem, de fogalmunk sem volt, hogy mi okozta a problémát, ugyanis "annak működnie kellett volna".

A verzió paralízis egy nagyon gyakorlati eredménye is lehet a sok-sok hibaüzenet. Pl. az alábbit egy Java 11 + Mockito kombó produkálja:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.mockito.cglib.core.ReflectUtils$2 (file:/C:/Users/Csaba/.m2/repository/org/mockito/mockito-all/1.10.19/mockito-all-1.10.19.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of org.mockito.cglib.core.ReflectUtils$2
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Bosszantó fordítási hibák

Van pár, szerintem nagyon bosszantó fordítási hiba, melyek szerintem feleslegesen növelik a kód hosszát. Például:

int i = 5;
short s = i;

Lehet persze úgy érvelni, hogy az int nagyobb a short-nál, előfordulhat, hogy nem lehet az értéket veszteségmentesen behelyettesíteni, emiatt kell az explicit konverzió. Ám az így érvelőket kérem, magyarázzák meg a következőt, ami csont nélkül lefordul és lefut:

int big = 2_000_000_000 + 2_000_000_000;
System.out.println(big); // -294967296

Ha tökéletes lenne a rendszer, akkor lenne értelme az elvárt védelemnek, így viszont csak felesleges kódot követel a fejlesztőtől.

Egy másik példa:

float f = 5.0 / 2;

Az 5.0 ugyanis double. Azért ez működhetne is, akár!

A következő: az elérhetetlen kód fordítási hibát eredményez. Lehetne csak fordítási figyelmeztetés.

Szerintem az is zavaró, hogy ha valahol a projektben van egy fordítási hiba, akkor nem tudjuk a programot elindítani, még akkor sem, ha az a kód elméletileg sem elérhető. Pl. van két osztály, egy-egy main() függvénnyel, a két osztály nem használja egymást, és az egyikben van egy fordítási hiba.

Problémák az adatszerkezetekkel

A Java nyelv egyik nagy előnye a Java Collections Framework, amelyben már kezdetektől fogva megtalálhatóak a legfontosabb adatszerkezetek. Ám évtizedeken keresztül hiába igényelte a fejlesztő közösség a tuple (elem n-es) adatszerkezet bevezetését, a Java nyelv készítői ellenálltak az igénynek. Viszont ami a '90-es években még nagy dolog volt, az ma már természetes: a legfontosabb adatszerkezetek ma már szinte minden modern programozási nyelv alapkellékei, sőt, sok esetben nemcsak könyvtárként vannak jelen, hanem nyelvi elemként is.

Apróság, de személy szerint engem zavar, hogy ha szeretnénk használni mondjuk egy listát, akkor külön importálni kell a java.util.List osztályt; jó lenne, ha a java.lang-hoz hasonlóan ez is automatikusan ott lenne.

Problémák a streamekkel

A Java 8-as verziójába végre belekerültek a streamek, ám a megvalósítás hagy maga mögött kívánni valót. Meglehetősen komplikáltra sikeredett, helyenként logikátlan megoldásokkal. Például egy stringből karakter streamet a nem túl informatív chars() hívással lehet. Tekintsük az alábbi példát!

String str = "apple";
str
    .chars()
    .map(c -> Character.toUpperCase(c))
    .forEach(System.out::print);

Szépen lefordul, futáskor viszont a következő meglepetést tapasztaljuk:

6580807669

Kellő tapasztalat hiányában innen sok idő eltelhet, mire rájövünk a megoldásra:

String str = "apple";
str
    .chars()
    .mapToObj(c -> (char)c)
    .map(c -> Character.toUpperCase(c))
    .forEach(System.out::print);

A problémát az okozza, hogy a chars() úgy készít karakter streamet, hogy közben IntStream-et készít, azaz a stream a karakterek ASCII kódjai, számként kezelve. Vissza kell konvertálni karakterré, hogy megfelelően írja ki.

Problémák a kivételkezeléssel

A kivételkezelés hasznos dolog, és igazán a Java-ban ismerkedtem meg vele, viszont mai fejemmel azt gondolom, hogy jelentősen túl van ez komplikálva. Volt idő, amikor azt gondoltam, hogy milyen jó, hogy különválasztják az ellenőrzött és a nem ellenőrzött kivételeket, de valójában lehetne mindegyik alapból ellenőrizetlen, mint pl. a Scala-ban, vagy más nyelvekben. Számomra a legfájóbb az, hogy a Thread.sleep() metódus ellenőrzött kivételt dob, amit le kell kezelni. Szóval egy tisztességes sleep(1000) vagy - legyen no! - Thread.sleep(1000) helyett ezt kell írni:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Ez egy nagyon ritka eset: Linuxban a kill utasítással lehet a folyamatoknak jelzést küldeni; valójában ezt tudjuk a fenti kivétellel lekezelni.

Egy ellenpélda (ismétlem: nem vagyok híve az ellenőrzött kivételeknek, de ha már egyszer van…): furcsa, hogy pl. az Integer.parseInt() nem dob ellenőrzött kivételt, tehát az Integer.parseInt("Helló, világ!") simán átmegy a statikus rostán.

Dátum anomáliák

Egészen elképesztő az a katyvasz, amit dátumkezelés címszóval a Java nyelvben összehoztak!

Alapból ott van a java.util.Date osztály. Önmagában az még egész jó, hogy ha csak úgy példányosítjuk, akkor az adott időpillanatot veszi alapul. De konkrét dátumot csak nagyon nehézkesen tudunk beállítani:

  • Paraméterként megadhatjuk az 1970. január 1-je 0 óra 0 perc 0 másodperc óta eltelt ezredmásodperceket. Bravó, remek öltet!
  • Használhatunk parszert. Ehhez kell használnunk egy SimpleDateFormat-ot (ami nem mellesleg nem szálbiztos, én rengeteg nehezen felderíthető problémához vezet), melynek a parse() metódusa elkészíti nekünk a Date objektumot. Persze ez a parse() ellenőrzött kivételt dob…
  • Használhatjuk a Calendar osztályt és a GregorianCalendar-t, a kismillió beállítással. Ráadásul abszolút logikátlan módon: pl. a hónapokat 0-tól kezdi sorolni, tehát 0 a január, 1 a február, 2 a március stb.

Az időzónákról és egyéb finomságokról nem is beszélve. Persze a java.util.Date osztályban lépten-nyomon elavult (deprecated) eljárásokba bukunk.

Lássuk mindezt egy példán keresztül!

package datehandling;
 
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
 
public class DateHandlingExample {
 
    public static void main(String[] args) {
        String pattern = "yyyy-MM-dd";
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
        Date today = null;
        try {
            today = simpleDateFormat.parse("2021-02-28");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        if (today != null) {
            String date = simpleDateFormat.format(today);
            System.out.println(date);
        }
    }
}

A Calendar segítségével:

package datehandling;
 
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
 
public class GregorianCalendarExample {
 
    public static void main(String[] args) {
        Calendar calendar =  new GregorianCalendar();
        calendar.set(Calendar.YEAR, 2021);
        calendar.set(Calendar.MONTH, 1);
        calendar.set(Calendar.DAY_OF_MONTH, 28);
        Date today = calendar.getTime();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        System.out.println(simpleDateFormat.format(today));
    }
}

Tehát a február hónap sorszáma az 1.

Sajnos nagyon könnyű összekeverni a java.util.Date-et a java.sql.Date-tel. Igazán adhattak volna neki más nevet! Mindenesetre a hasonló név mögött egészen eltérő viselkedés rejlik.

A 8-as verzióban a nyelv fejlesztői gondoltak egy nagyot, és tovább bonyolították a helyzetet a java.time csomaggal: LocalDate, LocalDateTime, ZonedDateTime, OffsetDateTime - hogy csak egy párta említsek a remekbe szabott újítások közül azoknak, akiknek idáig még nem ment volna el a kedve attól, hogy Java-ban valaha dátumokkal foglalkozzon.

Külső könyvtárak is igyekeztek úrrá lenne a káoszon, és megjelentek a saját megoldásaikkal: Hogy csak kettőt említsek:

  • Az Apache Commons Lang DateUtils osztálya,
  • A Joda-Time könyvtár.

És hogyan kezelik ezt a jövő győztesei? A Pythonban így:

import datetime
today = datetime.datetime(2021, 2, 28)
print(today)

A fájlkezelés nehézkességei

import java.io.*;
import java.nio.charset.Charset;
 
class FileInputExample {
    public static void main(String[] args) {
        BufferedReader dis = null;
        String line = null;
        try {
            File file = new File("mydata.txt");
            FileInputStream fis = new FileInputStream(file);
            BufferedInputStream bis = new BufferedInputStream(fis);
            InputStreamReader isr = new InputStreamReader(bis, Charset.defaultCharset());
            dis = new BufferedReader(isr);
            while ((line = dis.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (dis != null) {
                try {
                    dis.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
        }
    }
}

Érdekességképpen: a fentivel megegyező Python kód az alábbi:

file = open('mydata.txt', 'r')
for line in file.readlines():
    print(line)
file.close()

Persze az élet nem ilyen "egyszerű"! A fenti megoldásnak azonban van egy hátulütője: ha IDE-ből futtatjuk, akkor a projekt gyökerébe kell tenni a fájlt, ha konzolról, akkor a target/classes/ könyvtárba (ahonnan egyébként automatikusan törlődik), ha pedig egy futtatható jar-t, akkor a target/-ből (szintúgy). Jó lenne rendet tenni: pl. bárhonnan a classpath-ról olvasson. Ilyen pl. az src/main/resources/. Lássunk erre egy másik, egy - elvileg egyszerűbb - módszert!

package mytest;
 
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
 
public class MyResourceReader {
 
    public static void main(String[] args) {
        MyResourceReader myResourceReader = new MyResourceReader();
        myResourceReader.readFileFormResourcees();
    }
 
    public void readFileFormResourcees() {
        try {
            ClassLoader classLoader = getClass().getClassLoader();
            URL resource = classLoader.getResource("mydata.txt");
            File file = new File(resource.toURI());
            List<String> lines = Files.readAllLines(file.toPath(), StandardCharsets.UTF_8);
            lines.forEach(System.out::println);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Eltartott egy darabig, mire sikerült fordítási hiba mentessé tenni! És miért kell ide a classloader, URL, URI meg ilyesmi? Küldői kérdések.

A konzolról történő beolvasás problémái

Hogyan olvasunk a konzolról Java-ban? A válasz: meglehetősen bonyolultan! Az alábbi példában egyetlen tizedes törtet szeretnénk beolvasni:

try {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String doubleStr = br.readLine();
    double d = Double.parseDouble(doubleStr);
    System.out.println(d);
} catch (IOException e) {
    e.printStackTrace();
}

Ugyanez Pythonban:

d = input()
print(d)

Rendben, ne legyünk igazságtalanok; a Java is egyszerűsített:

Scanner scanner = new Scanner(System.in);
double d = scanner.nextDouble();
scanner.close();
System.out.println(d);

Ezt lefuttatva egy csinos kis semmitmondó kivételt kapunk:

Exception in thread "main" java.util.InputMismatchException
    at java.base/java.util.Scanner.throwFor(Scanner.java:939)
    at java.base/java.util.Scanner.next(Scanner.java:1594)
    at java.base/java.util.Scanner.nextDouble(Scanner.java:2564)
    ...

A megoldás, amire magamtól nem sikerült rájönnöm:

Scanner scanner = new Scanner(System.in).useLocale(Locale.US);
double d = scanner.nextDouble();
scanner.close();
System.out.println(d);

Problémák az adatbázis kezeléssel

Egy adatbázis kapcsolat létrehozása nem egyszerű Javaban! Először is szükség van az adatbázis specifikus JDBC-re:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>dbexample</artifactId>
    <version>1.0</version>
 
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
    </dependencies>
</project>

A lekérdező kód meglehetősen összetett:

package dbexample;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class JdbcExample {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb?useSSL=false", "csaba", "farago")) {
            Statement selectStatement = connection.createStatement();
            ResultSet resultSet = selectStatement.executeQuery("SELECT name, age FROM person");
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");
                System.out.println(name + " (" + age + ")");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Korábban még egy ilyen sorra is szükség volt:

Class.forName("com.mysql.jdbc.Driver");

Nem úgy fogalmaznék, hogy milyen jó, hogy már most nem feltétlenül kell, hanem megbocsáthatatlan bűn volt az, hogy korábban kellett.

Ugyanez Pythonban:

import mysql.connector
 
mydb = mysql.connector.connect(
  host="localhost",
  user="csaba",
  password="farago",
  database="testdb"
)
 
mycursor = mydb.cursor()
mycursor.execute("SELECT name, age FROM person")
myresult = mycursor.fetchall()
for x in myresult:
  print(x)

A webfejlesztés hiányosságai

Java-ban eléggé nehézkes a webszerverek fejlesztése is: külső könyvtárak kellenek hozzá. Vegyük a legegyszerűbb Hello, world! alkalmazást! Tegyük fel, hogy nem szeretnénk web konténert használni; ez esetben szükség van jópár függőségre:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>jettyjerseyexample</artifactId>
    <version>1.0</version>
 
    <dependencies>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.4.12.RC2</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlet</artifactId>
            <version>9.4.12.RC2</version>
        </dependency>        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-util</artifactId>
            <version>9.4.12.RC2</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-jetty-http</artifactId>
            <version>2.25</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.core</groupId>
            <artifactId>jersey-server</artifactId>
            <version>2.25</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-servlet-core</artifactId>
            <version>2.25</version>
        </dependency>
    </dependencies>
</project>

A főprogramban elindítjuk a webszervert:

package hu.faragocsaba.jetty;
 
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.glassfish.jersey.servlet.ServletContainer;
 
public class JettyServer {
    public static void main(String[] args) throws Exception {
        Server server = new Server(8080);
 
        ServletContextHandler context = new ServletContextHandler(ServletContextHandler.NO_SESSIONS);
        context.setContextPath("/jettyjerseyexample");
        server.setHandler(context);
 
        ServletHolder servletHolder = context.addServlet(ServletContainer.class, "/rest/*");
        servletHolder.setInitOrder(1);
        servletHolder.setInitParameter("jersey.config.server.provider.packages", "hu.faragocsaba.rest");
 
        server.start();
        server.join();
    }
}

Majd a következőképpen szolgáljuk ki a kérést:

package hu.faragocsaba.rest;
 
import javax.ws.rs.*;
 
@Path("/hello")
public class Hello {
    @GET
    @Path("/sayhello/{name}")
    public String sayHello(@PathParam("name") String name) {
        String result = "Hello " + name;
        return result;
    }
}

A gyakorlatban itt még szükség van pár dologra:

  • Érdemes futtatható JAR-t készíteni, ez kb. megkétszerezi a pom.xml fájlt.
  • A leggyakoribb adatformátum a weben a JSON, ennek anomáliáiról egy külön fejezetben foglalkozunk.

És most tekintsük ugyanezt a programot NodeJS-ben!

const http = require('http');
 
const hostname = '127.0.0.1';
const port = 3000;
 
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});
 
server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

Ha az alkalmazás app.js, akkor az indítás minden bohóckodás nélkül ennyi:

node app.js

Alap könyvtárak hiányosságai

A Java alap könyvtárak meglehetősen gazdagok; amikor a Java létrejött, akkor különösen azok voltak, mondjuk egy C++-hoz viszonyítva. Azonban számomra érthetetlen módon bizonyos alap funkciókhoz nincsenek belső könyvtárak pl.:

  • Naplózás.
  • Egységtesztelés.
  • JSON konverzió.
  • Web szerver létrehozása.

A félrecsúszott grafikus felhasználói felület

A Java megjelenésekor személy szerint leginkább a grafikus felületre csodálkoztam rá. C++-ban abszolút operációs rendszer függő volt a dolog, és akár egy MFC, akár egy X11 igen bonyolult volt. A Java-ban egységes grafikus felület van minden operációs rendszer alatt. Akkor még AWT volt, ill. később Swing, időközben viszont félig-meddig szabvánnyá vált a JavaFX. Ami egy ideig része volt az alap disztribúciónak, majd kivették belőle. A JavaFX-ben a kezdeti lépések összehasonlíthatatlanul bonyolultabbak, mint korábban voltak. Ráadásul az eredmény platformfüggő lesz.

Egészen elképesztő, hogy mi mindent kell beállítani ahhoz, hogy működjön! Különösen a 11-es Java-tól kezdve. Maven nélkül futtatható változatot, vagy Eclipse-ből indulót több órás kínlódást követően sikerült csak készítenem! Letöltöttem dolgokat, beállítottam, telepítettem, újraindítottam, ezáltal elrontottam, de egyik hiba jött a másik után…

És Pythonban? Ennyike:

from tkinter import *
root = Tk()
root.mainloop()

Parancssor hiánya

Számos nyelvben (pl. Python, Scala, R stb.) van lehetőség az utasítások parancssori kiadására (is). Ez egy nagyon komoly kényelmi funkció a lehetőségek gyors kipróbálásra. Java-ban erre nincs lehetőség, ami jelentősen lelassíthatja a fejlesztést. Pl. mindenképpen fordítási hibamentessé kell tenni a teljes projektet. Ill. ha még nincs, létre kell hozni azt, a forrást le kell menteni stb. IDE-ben is megfelelően be kell állítani, parancssorból meg külön kell fordítani és futtatni. De pl. egy DataBricks-ben mindenképpen kell készíteni jar-t (ami nem egyszerű), azt fel kell tölteni, és csak úgy lehet indítani.

Natív hiánya

A C/C++-nak meg van az az előnye, hogy ahol futtatjuk, ott nincs szükség telepíteni semmit. Az eredmény persze ezáltal platformfüggő lesz, viszont a program használóját nem kényszeríti semmi arra, hogy bármi extrát feltelepítsen. A legtöbb nyelvben kell valamit telepíteni (pl. Python, Perl, NodeJS, R stb. sőt, még a JavaScript-hez is kell böngésző), szóval ez önmagában nem jelent komoly versenyhátrányt, de előnyt sem. És a Java (JRE) telepítése nehézkesebb, mint a fent említettek.

A JRE igen erőforrás igényes, így kisebb teljesítményű számítógépek (pl. Orange PI PC) esetén nem is áll rendelkezésre, ahol pl. a Python használható. A JRE telepítése egyébként is sokkal nehézkesebb, mint más hasonló rendszereké (pl. Python, NodeJS, Perl).

A JDK-é még inkább. A letöltéséhez egy időben szükség volt Oracle azonosítóra. Amit az ember nem használ olyan gyakran, hogy megjegyezze a jelszavát, vagy akár az azonosítóját, és mire sikerült belépnie, elég hosszú idő eltelik. Mindezt csak amiatt, hogy elfogadjuk a licensz szerződést. Szerencsére időközben rájöttek arra, hogy ez azonosító nélkül is megy. Viszont a letöltő oldalon is a Windows-os változat (a Windows több mint 90%-os piaci részesedéssel rendelkezik!) a 9 elemből a 8. helyen van az a változat, ami nekünk kell. Összehasonlításul: tetszőleges más rendszer esetén a főoldalon ott "világít" az adott operációs rendszerre vonatkozó letöltő link.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License