Przejdziemy teraz do drugiej części trudnego do przyswojenia tematu jakim są typy generyczne. Java posiada szerokie pole do popisu, jeśli chodzi o programowanie uogólnione, bo tak to się elegancko nazywa. Kolejnym krokiem w ich prawidłowym rozumieniu są metody generyczne w języku Java. Jak je tworzyć, jak je rozumieć a co najważniejsze, jakie dają korzyści. Wszyscy zainteresowani niech zaglądają do środka!
Tweet |
METODY GENERYCZNE W JĘZYKU JAVA NIE DZIAŁAJĄ TAK, JAK MOŻESZ POMYŚLEĆ!
Może to być zamiennie stosowane. Metody generyczne to metody uogólnione i na odwrót. Aby zrozumieć ich sens zacznę od podania pewnego podchwytliwego przypadku. Wyobraźmy sobie, że mamy tablicę typu klasowego:
JPanel[] panels;
Zgodnie z polimorfizmem, możemy podstawić klasy potomne do zmiennej jako klasy macierzystej, a nawet abstrakcyjnej. Jeżeli metoda pobiera w parametrze tablicę typu "JComponent" (bardziej ogólnego), to nie będzie żadnych przeszkód dla kompilatora, żeby wstawić tablicę typu klasy potomnej jak ta powyżej.
A teraz spójrzmy na to:
ArrayList<JPanel> panels;
To jest kolekcja. Ona dysponuje o wiele większymi rygorami niż zwykła tablica. Jak myślicie, czy metoda zawierająca poniższą deklarację:
private void printComponents(ArrayList<JComponent> components)
łyknie wprowadzenie listy typu "JPanel"? NIE! Metody generyczne w języku Java posiadają silną podstawę dla kolekcji (ale nie tylko) i zapewnienie wysokiego poziomu bezpieczeństwa stanowi najważniejszy punkt ich wykorzystywania. Przy tablicy nie było problemu, ale warto zaznaczyć, że nie było ich z poziomu kompilatora. Zatem, nie mamy pewności, że podczas działania programu nie dojdzie do jakichkolwiek powikłań wynikających z naszych niedopatrzeń.
Chcę jeszcze napisać, że mimo tego, dopuszczalne jest dodawanie do kolekcji obiektów klasy potomnej, które pochodzą od klasy macierzystej będącej typem kolekcji. Lekcja z tego jest następująca: sto razy lepiej wychwycić błąd podczas pisania kodu niż przy uruchomieniu aplikacji. Właśnie to zapewnią nam typy generyczne.
PROGRAMOWANIE METOD UOGÓLNIONYCH
Powyższy akapit pokazał powód stosowania metod uogólnionych. Jest to taka deklaracja nagłówka, która umożliwia podstawienie kolekcji o dowolnym typie pochodzącym od podanej przez nas klasy macierzystej albo jej samej. Widzimy na górze, że nie jesteśmy w stanie do parametru typu "kolekcja przyjmująca obiekty typu >>JComponent<<" osadzić kolekcji obiektów typów potomnych. Przy użyciu tradycyjnego zapisu tak się zrobić nie da. Ostrzegam, że wiele osób następujący zapis może mocno zszokować, natomiast to pomoże nam w rozwikłaniu zagadki. Aby móc osadzać kolekcje o typie klasy macierzystej lub jej DOWOLNEJ klasy potomnej, trzeba to zapisać tak:
private <T extends JComponent> void printComponents(ArrayList<T> components)
Jak pamiętacie zapewne z poprzedniej części, wielka pojedyncza litera otoczona nawiasami kątowymi jest rozpoznawalnym znakiem typów generycznych. To "dziwadło" stojące zaraz po modyfikatorze dostępu jest ścisłym określeniem warunku jaki musi spełniać typ kolekcji, żeby został "dopuszczony" przez wejście do metody. To działa jak swoista bramka, która sprawdza kto może wejść do jakiegoś pomieszczenia, a kto nie. Robimy to za pomocą znanego już nam słowa "extends" przy czym zaznaczam, że to samo słowo w przypadku typów ogólnych oznacza coś jeszcze. Po słowie kluczowym, podajemy klasę macierzystą, która ma stanowić "korzeń" dla wpuszczanych typów. Nikt nie powiedział, że to MUSI być klasa macierzysta, można też podać jedną z klas potomnych!
Dla przykładu to mógłby być jedynie personel jakiejś firmy. Personel w tym wypadku byłby klasą abstrakcyjną, a na klasy potomne sprowadzałyby się osobne stanowiska. Gdyby metoda została napisana w sposób tradycyjny, oznaczałoby to możliwość wpuszczenia tylko jednego "typu" pracownika. Natomiast wykorzystując metody generyczne w języku Java, zostanie dopuszczony każdy człowiek pracujący w tej firmie niezależnie od tego, czy jest prezesem, czy dozorcą.
Metody generyczne umożliwiają wprowadzenie kolekcji o typie będącym klasą macierzystą, bądź dowolną z jej klas potomnych jednocześnie wyznaczając pewien "zbiór" klas, jakie mają być zaakceptowane.
PRZYKŁAD KODU ŹRÓDŁOWEGO
Podsumujmy to sobie prostym przykładem z trzema metodami o innych nagłówkach. Skompilujcie to sobie i przekonajcie się czy nie będzie żadnych problemów.
KLASA "MAIN"
public class Main {
public static void main(String[] args) {
new Launcher();
}
}
KLASA "GENERICEXAMPLE"
public class GenericExample<T> {
private final T instance;
public GenericExample(T instance) {
this.instance = instance;
}
public T getInstance() {
return instance;
}
}
KLASA "LAUNCHER"
import java. util.ArrayList;
import javax.swing.*;
public class Launcher {
private GenericExample<JPanel> panelGenericExample;
private GenericExample<JButton> buttonGenericExample;
private JComponent[] componentsArray;
private JPanel[] panelsArray;
private ArrayList<JComponent> componentsList;
private ArrayList<JPanel> panelsList;
private ArrayList<JButton> buttonsList;
public Launcher() {
createInstances();
addToLists();
printLists();
}
private void createInstances() {
panelGenericExample = new GenericExample<>(new JPanel());
buttonGenericExample = new GenericExample<>(new JButton());
componentsArray = new JComponent[]{panelGenericExample.getInstance(), buttonGenericExample.getInstance()};
panelsArray = new JPanel[]{panelGenericExample.getInstance()};
componentsList = new ArrayList<>();
panelsList = new ArrayList<>();
buttonsList = new ArrayList<>();
}
private void addToLists() {
componentsList.add(panelGenericExample.getInstance());
componentsList.add(buttonGenericExample.getInstance());
panelsList.add(panelGenericExample.getInstance());
buttonsList.add(buttonGenericExample.getInstance());
}
private void printLists() {
printArray(componentsArray);
printArray(panelsArray);
printJComponentList(componentsList);
//printJComponentList(panelsList);
printGenericList(componentsList);
printGenericList(panelsList);
printGenericList(buttonsList);
}
private void printArray(JComponent[] components) {
System.out.println("WYPISYWANIE ELEMENTÓW TABLICY");
for (JComponent component : components) {
System.out.println(component);
}
}
private void printJComponentList(ArrayList<JComponent> components) {
System.out.println("WYPISYWANIE ELEMENTÓW LISTY KONKRETNEJ");
for (JComponent component : components) {
System.out.println(component);
}
}
private <T extends JComponent> void printGenericList(ArrayList<T> components) {
System.out.println("WYPISYWANIE ELEMENTÓW LISTY GENERYCZNEJ");
for (T t : components) {
System.out.println(t);
}
}
}
Wszystkie metody robią to samo, tylko wypisują szczegóły o każdym obiekcie występującym w tablicy / kolekcji. Spróbujcie usunąć komentarz z linijki kodu i zapewniam Was, że wystąpi błąd kompilacji właśnie ze względu na bezpieczeństwo typów. Typy generyczne stanowią potężną broń w rękach programisty, tylko trzeba uważać żeby siebie samego nie odstrzelić.
To wszystko na ten temat. Na 99% będziecie musieli sobie dać o wiele więcej czasu niż przejście przez ten artykuł, aby zrozumieć tego sens. Ale będzie warto. Interesuje Was dalszy ciąg tej serii? Klikać tutaj.