Java:Zákoutí jazyka

Z FI WIKI
Přejít na: navigace, hledání

Pro úspěšné zvládnutí jazyka je třeba zvládnout dvě věci - jazyk samotný a dostupné knihovny funkcí/tříd. V tomto tématu chci shromáždit některé méně známé rysy jazyka Java, které je ale dobré znát.

Dobrým zdrojem poučení jsou The Java Specialists' Newsletter a The Java Language Specification, Third Edition

Anotace

Anotace umožňují poznamenat do zdrojového kódu další informace, tzv. meta data. V Core API jsou tři:

Programátor si může definovat své vlastní, a v JavaEE se používají dost zhusta. V JRE se jich nachází docela hodně, třeba

Hierarchie výjimek

Společným předkem všech chyb a vyjímek, tedy všeho, co se dá vyhodit příkazem throw a zachytit příkazem catch je java.lang.Throwable. Potomci třídy java.lang.Error označují vážné chyby systému, se kterými programátor nemůže nic udělat, a proto nemá cenu se je snažit zachytit a ošetřit.

Potomci třídy java.lang.Exception jsou tzv. checked exceptions, vyjímky, které musí být deklarovány v signatuře metody, že mohou nastat, tj. např.

 
public void mojeMetoda() throws IOException, MojeException {
...
}

Speciálním případem jsou potomci třídy java.lang.RuntimeException, což jsou vyjímky tak časté, že nemá smysl je v signaturách metod deklarovat, například java.lang.NullPointerException. Tato anomálie (runtime vyjímky jsou potomkem Exception) vznikla ve spěchu při vydání první verze jazyka Java, a od té doby kvůli zpětné kompatibilitě zůstává.

Inicializace tříd

Málo známými rysy jazyka java jsou existence inicializátorů, statických inicializátorů a pořadí jejich volání společně s konstruktory. Nejlépe je to vidět na ukázce. Mějme třídu se statickou metodou počítající její vyvolání:

 
public class Citac {
    private static int c = 0;
 
    public static String pis(String msg) {
        c++;
        String s = " " + c + ": " + msg;
        System.out.println(s);
        return s;
    }
}

a tři třídy demonstrující pořadí volání při inicializaci:

 
import static Citac.pis;
 
class Dedecek {
    { pis("Dedecek init 1"); }
    static { pis("Dedecek static init"); }
    public Dedecek() { pis("Dedecek konstruktor"); }
    { pis("Dedecek init 2"); }
}
 
class Rodic extends Dedecek {
    { pis("Rodic init 1"); }
    static { pis("Rodic static init"); }
    public Rodic() { pis("Rodic konstruktor"); }
    { pis("Rodic init 2"); }
}
 
public class Inicializace extends Rodic {
    String a = pis("a");
    { pis("init 1"); }
    String b = pis("b");
    { pis("init 2"); }
    static String s1 = pis("s1");
    static { pis("static init 1"); }
    static String s2 = pis("s2");
    static { pis("static init 2"); }
    static String s3 = pis("s3");
    public Inicializace() { pis("konstruktor"); }
    String c = pis("c");
 
    public static void main(String[] args) {
        System.out.println("--");
        new Inicializace();
        System.out.println("--");
        new Inicializace();
    }
}

Při spuštění bude výstup následující:

 1: Dedecek static init
 2: Rodic static init
 3: s1
 4: static init 1
 5: s2
 6: static init 2
 7: s3
--
 8: Dedecek init 1
 9: Dedecek init 2
 10: Dedecek konstruktor
 11: Rodic init 1
 12: Rodic init 2
 13: Rodic konstruktor
 14: a
 15: init 1
 16: b
 17: init 2
 18: c
 19: konstruktor
--
 20: Dedecek init 1
 21: Dedecek init 2
 22: Dedecek konstruktor
 23: Rodic init 1
 24: Rodic init 2
 25: Rodic konstruktor
 26: a
 27: init 1
 28: b
 29: init 2
 30: c
 31: konstruktor

A nyní vysvětlení. Inicializátor je kód mezi složenými závorkami { }, může jich být více v jedné třídě, a je to vlastně stejné místo, v jakém se provádějí přiřazení do proměnných instance. Všechna přiřazení a inicializátory jsou kompilátorem sloučena do jednoho inicializátoru v pořadí ve zdrojovém kódu. Je tedy jedno zda napíšete

 
class A {
  int a = 1;
  String b = "B";
  { System.out.println("jedeme"); }
  float c = 3f;
}

nebo

 
class A {
  int a;
  String b;
  float c;
  {
    a = 1;
    b = "B";
    System.out.println("jedeme");
    c = 3f;
  }
}

Statický inicializátor je totéž co inicializátor, ale pro statické proměnné, proto je uveden jako static { }. Statické proměnné se nastavují při zavedení třídy do paměti, což se u naší třídy Inicializace stane ještě před spuštěním metody main().

Při vytvoření instance třídy se postupuje hierarchií předků odshora dolů, a vždy se provádí inicializátor a poté konstruktor dané třídy.

Této znalosti lze využít k některým trikům, například:

 
List<String> list = new ArrayList<String>() {{ add("A"); add("B"); add("C"); }};

Tento kód vytvoří anonymního potomka třídy ArrayList, a v jeho inicializátoru (všimněte si dvojitých závorek) volá metodu add(). V okamžiku provádění inicializátoru je nadřízená třída ArrayList již plně inicializována, je to tedy zcela korektní kód, šetřící dlouhé psaní.

Lepší je

 
List<String> list = Arrays.asList("A","B","C");

Vnitřní třídy a statické vnitřní třídy

Platí pravidlo, že v jednou souboru se zdrojovým kódem může být nanejvýš jedna třída s přístupem public. Lze však do třídy přidávat další vnitřní třídy, které jsou jí podřízené:

 
public class A {
  public class B {
    public class C {
    }
  }
 
  public static class D {
    public static class E {
    }
  }
}

Kompilátor pak vnitřní třídy pojmenuje tak, že před její jméno přidá jména nadřízených tříd oddělená znakem dollar, tj. vyprodukuje soubory A$B$C.class A$B.class A.class A$D.class A$D$E.class.

Instance vnitřních tříd bez modifikátoru static (anglicky inner classes) obsahují vždy odkaz na instanci obklopující třídy. Instance vnitřních tříd s modifikátorem static (tj. statické vnitřní třídy) takový odkaz neobsahují.

Trochu překvapivá je syntaxe volání konstruktorů u nestatických vnitřních tříd, protože potřebují odkaz na obklopující instanci:

 
A a = new A();
A.B b = a.new B();
A.B.C c= b.new C();
 
A.D d = new A.D();
A.D.E e = new A.D.E();

Vnitřní třídy mají přístup k privátním proměnným nadřízených tříd:

 
public class A {
    private int a = 1;
 
    public class B {
        private int b = a;
 
        public class C {
            private int c1 = a;
            private int c2 = b;
        }
    }
}

Častým problémem je, jak zavolat ve vnitřní třídě metodu obklopující třídy, pokud má vnitřní třída metodu stejného jména. Pak se přistupuje pomocí kvalifikátoru JménoObklopujícíTřídy.this, tj.

 
public class A {
 
    public String metoda() {
        return "A";
    }
 
    public class B {
        public String metoda() {
            return "B";
        }
 
        public class C {
            public String metoda() {
                return "C";
            }
 
            public void volani() {
                System.out.println("C=" + metoda());
                System.out.println("B=" + B.this.metoda());
                System.out.println("A=" + A.this.metoda());
            }
        }
    }
}

Anonymní vnitřní třídy

Mnohdy je výhodné vytvořit potomka nějaké třídy, případně implementaci nějakého interface, ale použít ho/ji jen jednou, takže je zbytečné vymýšlet jméno.

Proto existuje syntaxe vytvářející anonymní vnitřní třídu:

 
  new TridaNeboInterface() { 
    ... implementace anonymni tridy ...
  }

Kompilátor pak vytvoří vnitřní třídu, která bude pojmenovaná jen čísle, tedy např. A$1.class.

Například se to hodí při vytváření jednorázové implementace třídy Comparator, která umožnuje seřadit pole podle nezvyklých kritérií:

 
        String[] pole = new String[]{"A-1", "A-2", "A-10", "A-20"};
 
        Arrays.sort(pole);
        System.out.println("pole = " + Arrays.asList(pole));
 
        Arrays.sort(pole, new Comparator<String>() {
 
            public int compare(String o1, String o2) {
                String[] a1 = o1.split("-");
                String[] a2 = o2.split("-");
                if (a1[0].equals(a2[0])) {
                    return Integer.parseInt(a1[1]) - Integer.parseInt(a2[1]);
                } else {
                    return a1[0].compareTo(a2[0]);
                }
            }
 
        });
        System.out.println("pole = " + Arrays.asList(pole));

výsledek:

pole = [A-1, A-10, A-2, A-20]
pole = [A-1, A-2, A-10, A-20]

Anonymní třídy se používají jako náhrada za neexistenci closure v jazyce Java do verze7, od verze 8 byla přidána pod názvem lambda. Closure je bezejmenná funkce, která může být předávána jako argument, a může využívat okolních lokálních proměnných.

V Javě může anonymní třída využívat lokálních proměnných pouze pokud jsou final, tj.

 
final int a = 1;
 
ActionListener al = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        System.out.println("a="+a);
    }
};

Sčítání řetězců

Podle definice jazyka se sčítání řetězců (případně s jinými typy převedenými na řetězce) optimalizuje převodem na použití třídy java.lang.StringBuffer, tedy výraz

 
String a = "A" + 12 + "B" + objekt;

bude přeložen jako

 
String a = new StringBuffer("A").append(12).append("B").append(objekt).toString();

foreach a Iterable

Od verze 5 je v syntaxi jazyka nová kostrukce zvaná foreach, která ale, aby nebylo nutné zavádět nové klíčové slovo, používá syntaxi for(Typ t: iterable) { ... }.

Je možné ji využít pro pole a všechny objekty implementující interface java.lang.Iterable, což jsou typicky kolekce všeho druhu:

 
int[] pole = new int[] { 1, 2, 3, 5 };
for(int i:pole) {
  //...
}
 
Collection<String> kolekce = new ArrayList<String>();
for(String s : kolekce) {
  //...
}

Ovšem můžeme ho využít i pro naši vlastní třídu, pokud implementuje java.lang.Iterable:

 
public class Moje<T> implements Iterable<T> {
 
    private T[] pole;
 
    public Moje(T[] pole) {
        this.pole = pole;
    }
 
    public Iterator<T> iterator() {
        return Arrays.asList(pole).iterator();
    }
}
...
        String[] a = new String[]{"A", "B", "C"};
        Moje<String> ms = new Moje<String>(a);
        for (String s : ms) {
            System.out.println("s: " + s);
        }

Porovnávání řetězců a internalizace

Většina programátorů v Javě ví, že objekty se nemají porovnávat operátorem porovnání ==, který porovnává shodu ukazatelů, ale pomocí metody equals(). U řetězců to má ale jeden háček.

Podle definice jsou všechny řetězcové literály, tj. konstantní řetězce uvedené přímo ve zdrojovém kódu, tzv. internalizovány, takže stejné řetězce odkazují na tu stejnou instanci třídy String. Internalizace libovolného řetězce lze dosáhnout voláním metody java.lang.String.intern() na řetězci, internalizované řetězce pak lze porovnávat pomocí ==.

 
String s1 = new String("X");
String s2 = new String("X");
String s3 = "X";
String s4 = "X";
 
System.out.println("(s1==s2) = " + (s1 == s2));
System.out.println("(s1.equals(s2) = " + (s1.equals(s2)));
System.out.println("(s3==s4) = " + (s3 == s4));
System.out.println("(s3==s1.intern()) = " + (s3 == s1.intern()));

produkuje:

(s1==s2) = false
(s1.equals(s2) = true
(s3==s4) = true
(s3==s1.intern()) = true


Generické třídy a parametrizace

V javě jsou generické typy udělány přes tzv. erasure, což znamená, že generická třída se přeloží jen do jednoho souboru s příponou .class, takže v době běhu není dostupná informace, jaká třída byla použita jako parametr.

Potíže to dělá většinou když potřebujeme novou instanci parametrického typu, např. nejde přeložit toto:

public class Genericka<T> {
 
    public T newParameterInstance() {
        return new T(); //chyba - nelze preložit
    }
 
}

Dá se to vyřešit tím, že se třída typového parametru předá explicitně jako parametr, např. takto:

public class Genericka<T> {
 
    private Class<T> clazz;
 
    public Genericka(Class<T> clazz) {
        this.clazz = clazz;
    }
 
    public T newParameterInstance() throws IllegalAccessException, InstantiationException {
        return clazz.newInstance();
    }

Má to ale nevýhodu, že je nutné parametrizující třídu uvádět dvakrát, takto:

 
  Genericka<String> g = new Genericka<String>(String.class);

Existuje ale jedna finta, využívající toho, že tzv. parametrizovaná třída, tj. třída, která vznikla z generické třídy doplněním typového parametru, v sobě má zakompilované, jaký byl typový parametr. Detekci typu lze napsat takto:

public abstract class Genericka<T> {
 
    @SuppressWarnings({"unchecked"})
    public T newParameterInstance() throws IllegalAccessException, InstantiationException {
        Class<T> clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        return clazz.newInstance();
    }
 
}

a místo původní generické třídy je třeba používat její parametrizované anonymní potomky, všimněte si závorek {} za voláním konstruktoru:

 
 Genericka<String> g = new Genericka<String>() {};
 String s = g.newParameterInstance();

Pokud tento trik není jasný, uvědomte si, že třeba tato třída:

public class MujStringList extends ArrayList<String> {
    //tady nic
}

má vlastní soubor .class, ve kterém je zakompilováno, že je potomkem generické třídy ArrayList parametrizované třídou String. Vytvořením anonymního potomka třídy Genericka tak vytvoříme nový soubor .class, a tedy vytvoříme místo, kde může být informace o parametrizaci uložena.

Generické třídy a wildcards

Generické typy mohou být docela neintuitivní. Například mějme třídy s následující dědičností:

 
   //    Object
   //     /  \
   //     A   Z
   //    / \
   //   B  Y
   //  / \
   // C   D
 
    static class A { }
    static class B extends A { }
    static class C extends B { }
    static class D extends B { }
    static class Y extends A { }
    static class Z { }

Neintuitivní je toto - do kolekce parametrizované tzv. "upper bound wildcard" popisující potomka (extends) třídy B nelze přidat instanci potomka B !

Lze do proměnné kolekce parametrizované na potomka B přiřazovat kolekce parametrizované na potomky B.

 
        List<? extends B> l;
 
        // můžeme přiřadit List parametrizovaný na B a jeho potomky
        l = new ArrayList<B>();
        l = new ArrayList<C>();
        l = new ArrayList<D>();
 
        // do něj můžeme před přiřazením vložit i instanci D
        ArrayList<D> ld = new ArrayList<D>();
        ld.add(new D());
        l = ld;
 
        //vytáhnutý prvek je B nebo potomek
        B b = l.get(0);
 
        //ale po přiřazení nelze vložit nic
        l.add(new Object()); //chyba !
        l.add(new A()); //chyba !
        l.add(new B()); //chyba !
        l.add(new C()); //chyba !
        l.add(new D()); //chyba !

Srovnejte to s poli, u kterých stejný případ přes kompilátor projde, ale při běhu může dojít k vyjímce.

 
        B[] poleb = new C[1];
        poleb[0] = new D(); // projde kompilací, ale za běhu vyhodí ArrayStoreException  !
        // D je potomek B, ale není potomek C
 

Tahle situace se dá vyřešit pomocí tzv. "lower bound wildcard" se slovem super.

Do kolekce parametrizované na předka (super) B můžeme vkládat instance B a jeho potomků. Naopak nejde vkládat instance předka B a přiřadit kolekci parametrizovanou na potomka B.

 
        List<? super B> l;
 
        //můžeme přiřazovat List parametrizovaný B a jeho *předky*
        l = new ArrayList<Object>();
        l = new ArrayList<A>();
        l = new ArrayList<B>();
 
        //před přiřazením můžeme vložit cokoliv
        List<Object> la = new ArrayList<Object>();
        la.add(new A());
        la.add(new B());
        la.add(new C());
        l = la;
 
        //proto vytáhnutý prvek je Object nebo jeho potomek
        Object o = l.get(0);
 
        //vkládat můžeme instance B a jeho *potomků*
        l.add(new B());
        l.add(new C());
        l.add(new D());
 
        //nejde do Listu vložit instanci předka B
        l.add(new A()); //chyba !
 
        //nejde přiřadit List parametrizovaný na potomka B
        l = new ArrayList<D>(); //chyba !

Obecná rada je, že se má používat

  • <? extends B> pro zdroj dat
  • <? super B> pro cíl dat

například:

 
    static void copyWithWildcards(List<? extends B> src, List<? super B> dst) {
        for (B b : src) {
            dst.add(b);
        }
    }
 
    static void copyOnlyB(List<B> src, List<B> dst) {
        for (B b : src) {
            dst.add(b);
        }
    }
 
   ///...
        ArrayList<Object> listObj = new ArrayList<>();
        listObj.add(new Object());
        listObj.add(new A());
 
        List<B> listB = new ArrayList<B>();
        listB.add(new B());
        listB.add(new C());
        listB.add(new D());
 
        List<C> listC = new ArrayList<C>();
        listC.add(new C());
 
        copyWithWildcards(listC, listB);   // v pořádku
        copyWithWildcards(listB, listObj); // v pořádku
 
        copyOnlyB(listC, listB); //chyba v prvním argumentu !
        copyOnlyB(listB, listObj); //chyba v druhém argumentu !

Lambda

Od Javy 8 je v jazyku nový jev, převzatý z funkcionálních jazyků, tzv. lambda.

Například vstupní test pro PV168 lze v Javě 7 zapsat:

 
        int limit = Integer.parseInt(args[0]);
        InputStream is = new URL("http://www.fi.muni.cz/~xadamek2/data.txt").openStream();
        try (BufferedReader in = new BufferedReader(new InputStreamReader(is, "utf-8"))) {
            List<Person> people = new ArrayList<>();
            String line;
            while (((line=in.readLine())!=null)) {
                if(line.trim().isEmpty()) continue;
                String[] x = line.split(",", 4);
                int salary = Integer.parseInt(x[3]);
                if(salary>limit)
                    people.add(new Person(x[1],salary));
            }
            Collections.sort(people,new Comparator<Person>() {
                @Override
                public int compare(Person o1, Person o2) {
                    return o1.getLastName().compareTo(o2.getLastName());
                }
            });
            for(Person p : people) {
                System.out.println("p = " + p);
            }
        }

a v Javě 8 to lze zapsat stručněji:

 
        int limit = Integer.parseInt(args[0]);
        InputStream is = new URL("http://www.fi.muni.cz/~xadamek2/data.txt").openStream();
        try (BufferedReader in = new BufferedReader(new InputStreamReader(is, "utf-8"))) {
            in.lines()
                    .filter(s -> !s.trim().isEmpty())
                    .map(s -> s.split(",", 4))
                    .map(x -> new Person(x[1], Integer.parseInt(x[3])))
                    .filter(p -> p.getSalary() > limit)
                    .sorted((p1, p2) -> p1.getLastName().compareTo(p2.getLastName()))
                    .forEachOrdered(System.out::println);
        }

Garbage Collector

Velmi pěkné vysvětlení je na Java Garbage Collection Basics.