Főoldal

"Mérnököt a mérnöktől"

A Schönherz Bázis összeköti az állást kereső és állást kínáló mérnököket.

CV küldés

Küldj önéletrajzot! Gyorsan, egyszerűen.
Megjegyzésbe írd be a pozíció nevét.
CV küldés

Iratkozz fel hírlevelünkre!

Hírek

10 dolog, amit nem tudtál a Java-ról
10 dolog, amit nem tudtál a Java-ról
Szóval a kezdetek óta Java-val foglalkozol?
Ne feledd a napot, amikora neve még’Oak’ volt, amikor az OOP még egy felkapott téma volt, vagy amikor a C++-osok úgy gondolták, a Java-nak esélye sincs, és az Applet-ek még léteztek.
Lefogadom, hogy a következő dolgok fele se lesz ismerős számodra. Kezdjük a hetet pár meglepetéssel a Java belső működésével kapcsolatban.
 
1.      Nincs olyan hogy ellenőrzött kivétel
 
Ez így van! A JVM nem ismer ilyet, csak a Java.
Manapság mindenki egyetért azzal, hogy az ellenőrzött kivételek hibák voltak. Bruce Eckel a GeeCON záró előadásán Prágában úgy nyilatkozott, hogy nincs a Java-n kívül olyan nyelv, amely ellenőrzi a kivételeket. Már a Java 8 sem alkalmazza őket az új Streams API-ban (ami zavaró lehet, amikor a lambdáid IO-t vagy JDBC-t használnak.).
Bizonyítékot szeretnél arra, hogy a JVM nem használ ilyesmit? Próbáld ki az alábbi kódot:

public class Test {
 
    // No throws clause here
    public static void main(String[] args) {
        doThrow(new SQLException());
    }
 
    static void doThrow(Exception e) {
        Test.<RuntimeException> doThrow0(e);
    }
 
    @SuppressWarnings("unchecked")
    static <E extends Exception> 
    void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

Nemcsak a fordít, de az SQLException-t is dob, és még csak nem is kell Lombok @SneakyThrows-ját használnod.
További részletek a fentiekről ebben a cikkben olvashatók, vagy a Stack Overflow cikkében.
 
2.      Létrehozhatsz olyan túlterhelt függvényeket melyeknek csak a visszatérési értékei különböznek 
Ez nem fordul le, mi?

class Test {
    Object x() { return "abc"; }
    String x() { return "123"; }
}

Nem bizony. A Java nyelv függetlenűl attól hogy a visszatérési típusuk és kivétel dobási típusuk is különbözik, nem teszi lehetővé hogy egy osztályon belül ugyan azt a függvényt kétszer terheld túl.
De várjunk csak egy pillanatot! Nézd meg a Class.getMethod(String, Class...) Javadoc-ját, amely így szól:
„Ne feledd, hogy előfordulhat egynél több illesztési módszer is egy osztályban, mert amíg a Java nyelv tiltja egy osztály deklarációját az azonos aláírással rendelkező többféle osztályoknak, kivéve a különböző visszatérési típusokat, addig a Java virtuális gép nem. Így nagyobb rugalmassággal lehet használni a virtuális gépeket különböző nyelvi funkciókon. Például kovariáns visszatérési értékek valósíthatók meg híd módszerekkel; ekkor a híd módszer és a felülíró módszer ugyanazzal az aláírással, de különböző visszatérési típusokkal rendelkezne .”
Wow, ennek tényleg van értelme. Pontosan ez történik, ha az alábbi kódot futtatod:

abstract class Parent<T> {
    abstract T x();
}

class Child extends Parent<String> {
    @Override
    String x() { return "abc"; }
}

Nézd meg a generált byte kódod Child-ban:

  // Method descriptor #15 ()Ljava/lang/String;
  // Stack: 1, Locals: 1
  java.lang.String x();
    0  ldc <String "abc"> [16]
    2  areturn
      Line numbers:
        [pc: 0, line: 7]
      Local variable table:
        [pc: 0, pc: 3] local: this index: 0 type: Child
  
  // Method descriptor #18 ()Ljava/lang/Object;
  // Stack: 1, Locals: 1
  bridge synthetic java.lang.Object x();
    0  aload_0 [this]
    1  invokevirtual Child.x() : java.lang.String [19]
    4  areturn
      Line numbers:
        [pc: 0, line: 1]

Szóval ’T’ tényleg csak egy objektum a byte kódban. Ez könnyen belátható.
A synthetic bridge módszert aktuálisan generálja a compiler, mivel  a Parent.x() aláírás visszatérési típusa várhatóan objektum lesz bizonyos hívások esetén. Generikumokat hozzátenni híd módszer nélkül nem lett volna lehetséges binárisan kompatibilis módon. Így kisebb fájdalommal jár megváltoztatni a JVM-et, hogy engedélyezze ezt a funkciót (ami szintén lehetővé teszi a kovariáns felülírást, mint mellékhatást…) Okos, mi?
Jártas vagy a nyelvi sajátosságokban és a belső felépítésben? Itt találsz néhány érdekes részletet.
 
3.      Ezek mind-mind kétdimenziós tömbök!

class Test {
    int[][] a()  { return new int[0][]; }
    int[] b() [] { return new int[0][]; }
    int c() [][] { return new int[0][]; }
}

Igen, ez igaz. Még ha fejben végiggondolva nem is értjük meg rögtön a visszatérési típust a fenti módszerben, attól még egyformák! Hasonló a következő kódrészlet is:
class Test {
    int[][] a = {{}};
    int[] b[] = {{}};
    int c[][] = {{}};
}

Azt gondolod, hogy ez őrültség? Képzelj el egy JSR-308 típusú megjegyzést a fenti kódban. A szintaktikai lehetőségek száma végtelen!
@Target(ElementType.TYPE_USE)
@interface Crazy {}

class Test {
    @Crazy int[][]  a1 = {{}};
    int @Crazy [][] a2 = {{}};
    int[] @Crazy [] a3 = {{}};

    @Crazy int[] b1[]  = {{}};
    int @Crazy [] b2[] = {{}};
    int[] b3 @Crazy [] = {{}};

    @Crazy int c1[][]  = {{}};
    int c2 @Crazy [][] = {{}};
    int c3[] @Crazy [] = {{}};
}

„Írj kommenteket. Az eszköz misztikumát csak az ereje haladja meg.”
vagy más szóval:
„Amikor a négy hetes vakációm elött commitolok.”


 
Döntsd el Te, hogy melyik a legmegfelelőbb számodra a gyakorlatban.
 
4.      Nem kapsz feltételes kifejezést
 
Azt hiszed mindent tudsz a kivétlkezelésről? Hidd el nekem hogy közel sem. Azt hihetnénk hogy az alábbi kódrészlet
Object o1 = true ? new Integer(1) : new Double(2.0);

…megegyezik ezzel:
Object o2;

if (true)
    o2 = new Integer(1);
else
    o2 = new Double(2.0);

Pedig valójában nem. Futtassunk le egy rövid tesztet.
System.out.println(o1);
System.out.println(o2);

A kimeneten az alábbiak jelennek meg:
1.0
1

Igen! Ha "szükséges" a feltételes operátor megvalósítja a numerikus típusú promóciót, de nagyon erősen idézőjelbe kell tennünk a "szükségest". Gondolnád, hogy a program NullPointerException-t dob?
Integer i = new Integer(1);
if (i.equals(1))
    i = null;
Double d = new Double(2.0);
Object o = true ? i : d; // NullPointerException!
System.out.println(o);
További információ a fentiekről itt.
 
5.      Az összetett értékadó operátort  se ismered igazán!
 
Azt hiszed igen? Nézzük a következő két kódrészleteket:
i += j;
i = i + j;

Azonosnak kellene lenniük, igaz? De tudod mit? Nem azok!
A JLS meghatározása szerint:
„Az összetett értékadó operátorban E’ op = E” ekvivalens az E1 = (T)((E1) op (E2))-vel, ahol T E1 típusa, kivéve hogy E1 csak egyszer értékelődik át.”
Ez annyira szép, hogy idézem Peter Lawrey válaszát a Stack Overflow kérdésre:
„Jó példa erre a *= vagy a  /= használata.”
byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57

vagy
byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40

vagy
char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'

vagy
char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'

Jó mi?
 
6.      Random egész számok
 
Ez már keményebb dió lesz. Ne lesd még meg a megoldást! Próbálj a megoldásra magadtól rájönni.
Amikor futtatom a következő programot:
for (int i = 0; i < 10; i++) {
  System.out.println((Integer) i);
}

… ebből „néha” ezt kapom kimenetnek:
92
221
45
48
236
183
39
193
33
84

Hogyan lehetséges ez egyáltalán?
.
.
.
.
.
. spoiler… megoldás lejjebb…
.
.
.
.
.
Ok, a megoldást itt találod és köze van JDK integer cache felülírásának a visszatükröződéséhez, majd az auto-boxing és auto-unboxing használatához.
Ne próbáld ki otthon!
Értelmezehetjük így is:
„Amikor a négy hetes vakációm elött commitolok.”


 
7.      Goto
 
Ez az egyik kedvencem. A Java-nak van GOTO-ja!
Írd be:
int goto = 1;

Ennek eredménye:
Test.java:44: error: <identifier> expected
    int goto = 1;
       ^

Ez azért van, mert a goto egy nem használatos kulcsszó. 
 
De nem is ez az izgalmas része. Az izgalmas rész az, hogy implementálni tudod a goto-t break, continue és labellű blokkokkal.

Előre ugrás:
label: {
  // do stuff
  if (check) break label;
  // do more stuff
}

Bytekódban:
2  iload_1 [check]
3  ifeq 6          // Jumping forward
6  ..

Hátra ugrás:
label: do {
  // do stuff
  if (check) continue label;
  // do more stuff
  break label;
} while(true);

Bytekódban:
 2  iload_1 [check]
 3  ifeq 9
 6  goto 2          // Jumping backward
 9  ..

8.      Java-ban vannak a típusoknak álneveik (aliases)
 
Más nyelvekben (pl. Ceylon) nagyon könnyen meghatározhatjuk a típusok álneveit:
interface People => Set<Person>;

A ilyen módon készült People típus alkalmas lehet a váltakozó használatra a Set<Person>-nel:
People?      p1 = null;
Set<Person>? p2 = p1;
People?      p3 = p2;

A Java-ban a típusok álneveit nem tudjuk magasabb szinteken meghatározni. De egy class vagy függvény hatókörét igen. Vegyük úgy, hogy nem vagyunk elégedettek az Integer, Long stb. nevekkel, rövidebbeket akarunk: I és L.
Nem bonyolult:
class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " + 
            l.longValue()
        );
    }
}

A fenti programban az osztály hatókörén belül integer „álnevesítve” van I-re, míg a Long „álneve” L az x() metódusban. A fenti módszert így tudjuk meghívni:
new Test().x(1, 2L);

Ezt a technikát természetesen nem kell komolyan venni. Ebben az esetben integer és long egyaránt visszatérő típus, ami azt jelenti, hogy az I és L tényleges álnevek (Majdnem. Hozzárendelés-kompatibilitás csak egy irányba működik). Ha már használtuk a nem végleges típusokat (pl. Objektum), akkor ténylegesen normális generikumokat használnánk.
Elég ezekből a buta trükkökből. Most következzen valami igazán figyelemre méltó!

9.      Néhány kapcsolattípus eldönthetetlen!
 
Oké, ez most pörgős lesz, úgyhogy végy egy csésze kávét és koncentrálj. Nézzük a következő két típust:
// A helper type. You could also just use List
interface Type<T> {}

class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

Nos, mit is jelent a C és D típus?
Valamennyire rekurzívak, hasonlóan (mégis kicsit eltérően) a java.lang.Enum-hoz.
Gondoljuk csak át:
public abstract class Enum<E extends Enum<E>> { ... }

A fenti specifikációnak megfelelően a tényleges enum végrehajtása pusztán szintaktikai finomság:
// This
enum MyEnum {}

// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }

Ezt szem előtt tartva térjünk vissza a két típushoz. Vajon a következő lefordul?
class Test {
    Type<? super C> c = new C();
    Type<? super D<Byte>> d = new D<Byte>();
}

Nehéz kérdés, de Ross Tate tudja a választ.
A kérdés valójában eldönthetetlen:
 
Vajon C altípusa <? super C> típusnak?
Step 0) C <?: Type<? super C>
Step 1) Type<Type<? super C>> <?: Type (inheritance)
Step 2) C  (checking wildcard ? super C)
Step . . . (cycle forever)

Vajon D altípusa <? super D<Byte>> típusnak?
Step 0) D<Byte> <?: Type<? super C<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
Step 2) D<Byte> <?: Type<? super D<D<Byte>>>
Step 3) List<List<? super C<C>>> <?: List<? super C<C>>
Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
Step . . . (expand forever)

Próbáld meg lefordítani a fentit Eclipse-ben… le fog fagyni. (Ne aggódj, tettem bele egy bugot)
Hagyjuk ezt kicsit leülepedni…
„Néhány típuskapcsolat a Java-ban eldönthetetlen!”
Ha részletesebben is érdekelnek a különleges Java trükkök, olvasd el Ross Tate irományát a „Taming Wildcards in Java’s Type System”-t (társszerzője Alan Leung és Sorin Lerner), vagy a mi tűnődéseinket a korreláló altípus polimorfizmusát a generikus polimorfizmussal.
 
10.  Típus kereszteződések (intersections)
 
A Java-nak egy nagyon jellegzetes tulajdonsága az úgynevezett típus kereszteződések. Deklarálhatsz egy (generikus) típust, ami tulajdonképpen kereszteződésben álló két típus. Például:
class Test<T extends Serializable & Cloneable> {
}

A generikus típus paraméter T, amit a Test osztályhoz kötünk, hogy másolhatóvá és szálasíthatóvá implementáljunk. Például a String nem lehet kötött, de a Dátum igen:
// Doesn't compile
Test<String> s = null;

// Compiles
Test<Date> d = null;

Ezt a funkciót használja újra a Java 8, ahol egy ad-hoc típusú kereszteződést ömlesztesz. Hogy hasznos-e ez? Szinte egyáltalán nem, de ha azt szeretnénk, hogy kényszerítse a lambda kifejezést ebben a típusban, nincs más lehetőség.
Tegyük fel, hogy van ebben az őrült típusban egy akadálymentes módszer:
<T extends Runnable & Serializable> void execute(T t) {}

Te egy futtatható és sorosan végrehajtható programot szeretnél, úgy, hogy valahol máshol hajtsa végre és küldje át a vezetéken.
A lambdák sorszámozása egy kicsit trükkös:
A lambdákat lehet szerializálni:
Akkor szerializálhatod a lambda kifejezést, ha a cél típusa és a bevett argumentumok szerializálhatók.
De még ha ez igaz is, nem automatikusan implementálhatók a szerializáló felületen. Kényszeríteni kell őket, hogy az ilyen típust mindig hozza. De ha csak szerializálsz…
execute((Serializable) (() -> {}));

… akkor a lambda már nem lesz futtatható.
Ehh…
Szóval…
Vezessétek be mintkét típust:
execute((Runnable & Serializable) (() -> {}));

Összegzés:
Általában csak az SQL-ről szoktam ezt mondnai, de itt az ideje, hogy az alábbival zárjunk a cikket:
„A Java egy olyan eszköz, amelynek rejtélyét csak az ereje haladja meg.”
 
(forrás: jOOQ)
 
***
 
Ha kreatív, kihívásokkal teli, minőségi mérnök állást keresel minőségi munkáltatónál, jó helyen jársz, mert a Schönherz Bázis épp azért jött létre, hogy Neked segítsen.
Gyere, nézz szét aktuális állásaink között!

Schönherz Bázis – Jó mérnököt jó helyre!
 
***
 
#programnyelvek #programozas #php #java #phyton #perl #javascript #mernokallas #informatikusallas #felmondas #elbocsatas #munkamaganelet #worklifebalance #maganelet #parkapcsolat #prioritas #karrier #karriervagycsalad #informatikusmunka #javafejleszto #cfejleszto #cprogramozo #seniorinformatikus #bmevik #schonherz #schonherzalumni #itszakerto #itspecialistjob #itjob
 
 
Oszd meg barátaiddal is!