Vnořené třídy

Vnořené, vnitřní a anonymní třídy

Vnořené

definované uvnitř jiné třídy jako její prvek - podobně jako metody nebo atributy.

Vnitřní

jako vnořené, ale potřebují ke své instanciaci objekt vnější třídy, v níž jsou definovány

Lokální

definované uvnitř metody podobně jako lokální proměnná

Anonymní

vnitřní, ovšem nepojmenované, pojmenuje si je překladač sám stylem Person$1

Vnořená třída

  • definované uvnitř jiné třídy jako její prvek - podobně jako metody nebo atributy

  • je uvozené klíčovým slovem static, pak se jedná o statickou vnořenou třídu

  • chybí-li static, jedná se o nestatickou vnořenou třídu označovanou jako vnitřní třída (inner class) viz dále

Statická vnořená třída

  • Angl.: static nested class

  • Lze ji mít jako private, pak není vidět zvenčí.

  • Přesto je použitelná v rámci atributů metod obklopující vnější třídy.

  • I opačně - statická vnořená třída může používat i privátní statické metody a atributy své vnější třídy.

Příklad BEZ statické vnořené třídy

Zřetězený seznam objektů typu String: Třída pro uzel

public class Node {
    private String element;
    private Node next;

    public Node(String element, Node next) {
       this.element = element;
       this.next = next;
    }

    public Node getNext() {
       return next;
    }

    public String getElement() {
       return element;
    }
}

Příklad BEZ statické vnořené třídy

Zřetězený seznam objektů typu String: Třída pro seznam

public class StringLinkedList {
    private Node head;

    public StringLinkedList(String[] strings) {
        for (String str: strings) {
            head = new Node(str, head);
        }
    }

    public boolean contains(String str) {
        Node node = head;
        while (node != null) {
            if (node.getElement().equals(str)) {
                return true;
            }
            node = node.getNext();
        }
        return false;
    }
}

Příklad SE statickou vnořenou třídou

public class StringLinkedList {
    private Node head;

    public StringLinkedList(String[] strings) {
        for (String str: strings) {
            head = new Node(str, head);
        }
    }

    public boolean contains(String str) {
        Node node = head;
        while (node != null) {
            if (node.element.equals(str)) { // access to element is OK
                return true;
            }
            node = node.next; // access to next is OK
        }
        return false;
    }

    private static class Node { // getters are not required here
        private String element;
        private Node next;

        public Node(String element, Node next) {
            this.element = element;
            this.next = next;
        }
    }
}

Příklad veřejné statické vnořené Map.Entry

public class MapDemo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        for(Map.Entry<String, String> entry: map.entries()) {
            // do something for each (key, value) pair:
            // entry.getKey(), entry.getValue()
            // or entry.setValue(...)
        }
    }
}

Vnitřní

  • Vnitřní = nestatická vnořená třída

  • Angl.: nested inner class

  • Objekt takové třídy potřebuje ke své instanciaci objekt své vnější třídy.

  • Veřejná statická vnořená: Inner inner = new Outer.Inner();

  • Vnitřní:

    Outer outer = new Outer();
    Inner inner = outer.new Inner();

Příklad BEZ vnitřní třídy

Iterovatelné produkty: Motivace

public class Shop {
    private String[] products = new String[] {"product 1", "product 2", "product 3"};
    private int pos = 0;

    public String getProduct(int index) {
        if (index < 0 || index >= products.length) {
            return null;
        }
        return products[index];
    }

    public boolean hasNext() {
        return (pos < products.length);
    }

    public String next() {
        return products[pos++];
    }

    public void goToStart() {
        pos = 0;
    }
}

Příklad BEZ vnitřní třídy

Iterovatelné produkty: Třída iterátoru

public class ExternalIterator {
    private Shop shop;
    private int pos = 0;

    public ExternalIterator(Shop shop) {
        this.shop = shop;
    }

    public boolean hasNext() {
        return (pos < shop.getProducts().length);
    }

    public String next() {
        return shop.getProducts()[pos++];
    }

}

Příklad BEZ vnitřní třídy

Iterovatelné produkty: Třída obchodu

public class Shop
{
    private String[] products = new String[] {"product 1", "product 2", "product 3"};

    public ExternalIterator getIterator() {
        return new ExternalIterator(this);
    }

    public String[] getProducts() {
        return products;
    }
}

Příklad BEZ vnitřní třídy

Iterovatelné produkty: Příklad použití

Shop shop = new Shop();
ExternalIterator iter = shop.getIterator();
while (iter.hasNext()) {
    System.out.print(iter.next() + " ");
}

Příklad S vnitřní třídou

public class Shop {
    private String[] products = new String[] {"product 1", "product 2", "product 3"};

    public Iterator getIterator() {
        return new Iterator();
    }

    public class Iterator {
        private int pos = 0;

        public boolean hasNext() {
	    return (pos < products.length);
	}

        public String next() {
	    return products[pos++];
	}
    }
}
Shop shop = new Shop();
Shop.Iterator iter = shop.getIterator();
Shop.Iterator iter2 = shop.new Iterator(); // alternate way if the contructor is public
while (iter.hasNext()) {
    System.out.print(iter.next() + " ");
}

Příklad S vnitřní třídou

  • U vnitřních tříd padají omezení kladená na statické vnořené třídy

  • Vnitřní třída má přístup k privátním prvkům vnější třídy

  • Pak ale může nastat kolize jmen, která je povolená

public class ShadowTest {
    private int x = 0;

    public class FirstLevel {
        private int x = 1;

        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }

    public static void main(String[] args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

Omezení pro vnitřní (nestatické) třídy

  • Ve vnitřní třídě nemůžeme mít statické metody, protože vnitřní třída je implicitně spojena s objektem své vnější třídy. Nemůže tak definovat žádnou statickou metodu pro sebe.

  • Může mít statické atributy. Pozor, hodnoty jsou pak sdíleny napříč všemi instancemi vnitřní třídy bez ohledu na to, přes který vnější objekt byly vytvořeny.

Proč používat vnořené třídy (statické i nestatické)?

  • Logicky seskupují třídy, které se používají pouze na jednom místě: Pokud je třída užitečná pouze pro jednu jinou třídu, je logické ji do ní začlenit a nechat je pohromadě. Vnořování takových "pomocných tříd" zefektivňuje jejich balík.

  • Zvyšuje zapouzdření: Uvažujme dvě třídy nejvyšší úrovně, A a B, kde B potřebuje přístup ke členům A, které by jinak byly deklarovány jako soukromé. Skrytím třídy B do třídy A lze členy třídy A prohlásit za soukromé a třída B k nim může přistupovat. Kromě toho může být samotná třída B skryta před okolním světem.

  • To může vést k čitelnějšímu a udržovatelnějšímu kódu: Vnořování malých tříd do tříd nejvyšší úrovně přibližuje kód k místu jeho použití.

Poznámky k použití vnořených tříd

  • Běžně se používají vnořené výčtové typy

class Person {
  public enum Gender {
    MALE, FEMALE
  }
  • A to i uvnitř rozhraní

interface MyInterface {
  enum AnotherNestedEnum {
     CC1, CC2, CC3;
  }
  int operation1();
}

Kdy použít kterou variantu?

  • Pokud vnořená třída nepotřebuje přístup k žádným proměnným nadřazené třídy, může být statická. V opačném případě musí být vnitřní (nestatická).

  • Výhodou statické vnořené třídy je, že k jejímu použití nepotřebujete instanci vnější třídy. Je to tedy obecně pohodlnější a preferovanější varianta.

Lokální vnitřní třídy

  • Angl.: method local inner classes

  • Podobná jako vnitřní. Není ale definována v rámci nadřazené třídy, ale pouze některé její metody.

  • Má omezenější platnost/viditelnost (scope) pouze na danou metodu.

  • Může ale sahat na atributy nadřazené třídy.

public class Outer {
   int num = 23;
   public void display(){
      class Inner{
         public void print() {
            System.out.println("The number is " + num); // OK
         }
      }
      Inner obj = new Inner();
      obj.print();
   }
}

Anonymní třídy

  • Angl.: anonymous inner class, často je anonymous class

  • Mohou být vnitřní i lokální vnitřní (tj. definované v rámci třídy nebo metody), navíc jsou ale nepojmenované

  • Pozn.: Jak uvidíme později, kód některých anonymních tříd se dá zapsat velmi kompaktně pomocí lambda výrazů.

Omezení pro anonymní třídy

  • Vždy se vytváří jako rozšíření existující (tj. pojmenované) třídy nebo implementací existujícího (tj. pojmenovaného) rozhraní.

  • Z toho plyne, že anonymní třída může implementovat/rozšiřovat pouze jedno rozhraní/třídu.

  • Na jednom místě se definuje a zároveň i jednou použije (instanciuje). Pokud chceme instanciovat na více místech, musí vytvořit pojmenovanou třídu (nebo duplikovat kód, což nechceme).

Příklad anonymní třídy

interface Hello {
    void show();
}
public class Demo {
    private Hello hello = new Hello() {
        public void show() {
            System.out.println("i am in anonymous class");
        }
    };
}

Shrnutí typů vnořených tříd

Statická vnořená třída

Je to statická třída, která je definována uvnitř jiné třídy. Nemá přístup k nestatickým členům vnější třídy.

Vnitřní třída

Je to nestatická třída, která je instanciována skrze existující vnější objekt. Má přístup ke všem atributům a metodám vnější třídy, včetně soukromých.

Lokální vnitřní třída: Je to třída, která je definována uvnitř metody nebo bloku kódu. Má přístup ke proměnným v bloku, ve kterém je definována.

Anonymní vnitřní třída: Je to třída, která je definována inline a nemá žádné jméno. Používá se k implementaci rozhraní nebo rozšíření tříd bez nutnosti vytvoření samostatné třídy.

Shrnutí výhod vnořených tříd

Zapouzdření

Vnitřní třídy mají přístup k soukromým proměnným a metodám vnější třídy. To pomáhá dosáhnout zapouzdření a zlepšuje čitelnost kódu.

Organizace kódu

Vnitřní třídy umožňují seskupit související kód na jednom místě. Díky tomu je kód přehlednější a snadněji se udržuje.

Lepší řízení přístupu

Vnitřní třídy lze deklarovat jako soukromé, což znamená, že k nim lze přistupovat pouze v rámci vnější třídy. To poskytuje lepší řízení přístupu a zvyšuje bezpečnost kódu.

Zpětná volání

Vnitřní třídy se často používají pro implementaci zpětných volání v programování řízeném událostmi (event-driven programming). Poskytují pohodlný způsob, jak definovat a implementovat funkci zpětného volání v kontextu vnější třídy.

Polymorfismus

Vnitřní třídy lze použít k implementaci polymorfismu. V rámci vnější třídy lze definovat hierarchii tříd a poté vytvořit objekty vnitřních tříd, které implementují různé podtřídy.

Snížení složitosti kódu

Vnitřní třídy mohou snížit složitost kódu zapouzdřením složité logiky a datových struktur v kontextu vnější třídy.

Celkově může použití vnitřních tříd vést k modulárnějšímu, udržovatelnějšímu a flexibilnějšímu kódu.