PO Typy uogólnione: Różnice pomiędzy wersjami
Nie podano opisu zmian |
|||
Linia 104: | Linia 104: | ||
} | } | ||
Teraz spróbujmy utworzyć Osobę, przechować na stosie i pobrać | |||
do dalszego przetwarzania: | |||
Osoba os1 = new Osoba("Jasio"); | |||
Stos s = new Stos(); | |||
s.wstaw(os1); | |||
Osoba os2 = s.pobierz(); | |||
Niestety ostatni wiersz się nie skompiluje. Powód jest | |||
prosty - metoda pobierz ma typ wyniku Object, a my potrzebujemy | |||
obiektu klasy Osoba. My wprawdzie wiemy, że na stosie jest | |||
Osoba, ale kompilator tego | |||
nie wie.<ref name="PO_kompilator_nie_wie">Jeśli nie jesteś przekonany | |||
dlaczego kompilator tego nie jest w stanie przewidzieć wyobraź | |||
sobie następującę: przed wstawieniem na stos pytamy użytkownika, czy | |||
wstawić Osobę czy np. obiekt klasy Nietoperz i wstawiamy obiekt wybrany | |||
przez użytkownika. Teraz przy pobieraniu ze stosu nawet my (twórcy | |||
programu) nie wiemy, czy dostaniemy Osobę czy Nietoperza.</ref> | |||
Jak można temu zaradzić? Jedynym rozwiązaniem dostępnym | |||
w wczesniejszych wersjach Javy było zastosowanie rzutowania typów: | |||
Osoba os2 = (Osoba) s.pobierz(); | |||
Jest to jednak bardzo niedobre rozwiązanie - w tym momencie rezygnujemy | |||
z automatycznego sprawdzania typów wykonywanego przez kompilator i każemy | |||
mu uwierzyć (a nie wyliczyć), że typ obiektu pobieranego ze stosu jest | |||
właściwy. | |||
Dlatego w wielu językach programowania ze statyczną kontrolą zgodności | |||
typów wprowadzono pojęcie klas lub funkcji uogólnionych (ang. generics). | |||
Pomysł jest bardzo prosty, chcemy sparametryzować fragment kodu typem | |||
przetwarzanych danych, więc dodajemy nowy rodzaj parametrów opisujących | |||
typy. | |||
Zanim dokładniej przyjrzymy się rozwiązaniu tego problemu w Javie, warto | |||
zaznaczyć, że realizacje klas uogólnionych we współczesnych językach | |||
obiektowych dość istotnie różnią się od siebie.<ref name="PO_typy_funkcyjne"> | |||
A jeszcze inne podejście do tego problemu można znaleźć np. w realizacji | |||
funkcji uogólnionych w językach funkcyjnych.</ref>Rozwiązanie przyjęte | |||
w C++ opiera się na bardzo prostej idei (z grubsza rzecz biorąc sprowadza | |||
się do rozwijania makr) ma natomiast olbrzymią siłę wyrazu (np. umożliwia makroprogramowanie). Kwestią do dyskusji pozostaje, czy tak duża siła wyrazu | |||
tego mechanizmu rzeczywiście jest potrzebna. W Javie zastosowano zupełnie | |||
inne rozwiązanie, oparte o ideę wymazywania typów (ang. type erasure), | |||
co było wymuszone koniecznością zachowania zgodności z wcześniejszymi | |||
wersjami (przede wszystkim chodziło o zgodność z dotychczasową maszyną | |||
wirtualną). Zaproponowane rozwiązanie jest zgrabne i wystarczające | |||
w większości przypadków, jest ma szereg irytujących ograniczeń, których | |||
przyczyny nie można zrozumieć bez odwołania się do zastosowanego mechanizmu wymazywania typów. Wreszcie C# oferuje rozwiązanie bardzo podobne do tego | |||
z Javy, jednak dzięki temu, że twórcy nie byli zmuszeni do zachowania | |||
zgodności z poprzednimi wersjami maszyny wirtualnej, jest ono bardziej | |||
spójne. | |||
== Klasy parametryzowane typami == | == Klasy parametryzowane typami == |
Wersja z 10:08, 6 sie 2006
(moduł w trakcie pisania)
(wymagania wstępne:
reguła podstawiania (dziedziczenie) typy referencyjne
)
Typy uogólnione
Wprowadzenie
Siłą programowania obiektowego jest łatwość opisywania w nim i tworzenia abstrakcji. Pojęcie klasy pozwala tworzyć abstrakcje, zaś mechanizm dziedziczenia ułatwia tworzenie nowych abstrakcji na podstawie już istniejących.
Czasami jednak okazuje się, że te narzędzia nie zaspokajają jeszcze wszystkich naszych potrzeb. Niejednokrotnie tworzymy pojęcia, które chcielibyśmy nie tylko sparametryzować występującymi w nich wartościami (co czynimy wprowadzając do klas atrybuty), czy czynnościami (co pozwalają nam osiągnąć metody), lecz także typami obiektów przechowywanych lub przetwarzanych przez klasy.
Na pierwszy rzut oka może się wydawać, że sam fakt dysponowania mechanizmem dziedziczenia i regułą podstawiania pozwala na abstrahowanie do typu przetwarzanych obiektów. Rzeczywiście możemy zadeklarować, że dane przechowywane w naszej klasie są typu Object i dzięki regule podstawiania umieszczać w tak zedeklarowanych atrybutach wartości dowolnego typu referencyjnego <ref name="PO_slaba_Java"> Ze względu na specyfikę Javy musimy ograniczyć się tu tylko do typów referencyjnych, także rozwiązanie przyjęte w Javie - które dalej omawiamy - ma tę niedogodność.</ref> Zobaczmy dlaczego takie rozwiązanie jest niedobre na przykładzie fragmentu deklaracji klasy Stos. Chcemy mieć tradycyjny stos przechowujący wartości dowolnego typu referencyjnego. Możemy w tym celu stworzyć listę elementów stosu. Elementy Stosu zapamiętamy w obiektach klasy EltStosu, zaś sam obiekt klasy Stos będzie pamietał jedynie pierwszy element listy.
Oto przykładowa implementacja (z pominiętymi mniej istotnymi elementami):
public class Stos { private EltStosu wierzch; public Stos() { wierzch = null; } public boolean pusty(){return wierzch == null;} public void wstaw(Object elt){ wierzch = new EltStosu(elt, wierzch);} public Object pobierz() throws PustyStos { if (pusty()) throw new PustyStos(); Object wynik = wierzch.elt; wierzch = wierzch.nast; return wynik; } }
class EltStosu { public final Object elt; public final EltStosu nast; // Ponieważ obiekty klasy EltStosu są ukryte w Stosie, nie // ma potrzeby deklarowania ich atrybutów jako prywatnych. // Ponadto deklaracja final daje tu zwiększone bezpieczeństwo. /** Creates a new instance of EltStosu */ public EltStosu(Object elt, EltStosu nast) { this.elt = elt; this.nast = nast; } }
class PustyStos extends Exception{}
Na razie wszystko wyglada dobrze, ale spróbujmy skorzystać z naszej impelmentacji stosu. Załóżmy, że mamy klasę Osoba, zawierającą m.in. pole Imię (pozostałe atrybuty i metody w tym miejscu pomijamy):
public class Osoba { private String imię; public Osoba(String imię) { this.imię = imię; } public String imię(){ return imię; } }
Teraz spróbujmy utworzyć Osobę, przechować na stosie i pobrać do dalszego przetwarzania:
Osoba os1 = new Osoba("Jasio"); Stos s = new Stos(); s.wstaw(os1); Osoba os2 = s.pobierz();
Niestety ostatni wiersz się nie skompiluje. Powód jest prosty - metoda pobierz ma typ wyniku Object, a my potrzebujemy obiektu klasy Osoba. My wprawdzie wiemy, że na stosie jest Osoba, ale kompilator tego nie wie.<ref name="PO_kompilator_nie_wie">Jeśli nie jesteś przekonany dlaczego kompilator tego nie jest w stanie przewidzieć wyobraź sobie następującę: przed wstawieniem na stos pytamy użytkownika, czy wstawić Osobę czy np. obiekt klasy Nietoperz i wstawiamy obiekt wybrany przez użytkownika. Teraz przy pobieraniu ze stosu nawet my (twórcy programu) nie wiemy, czy dostaniemy Osobę czy Nietoperza.</ref>
Jak można temu zaradzić? Jedynym rozwiązaniem dostępnym w wczesniejszych wersjach Javy było zastosowanie rzutowania typów:
Osoba os2 = (Osoba) s.pobierz();
Jest to jednak bardzo niedobre rozwiązanie - w tym momencie rezygnujemy z automatycznego sprawdzania typów wykonywanego przez kompilator i każemy mu uwierzyć (a nie wyliczyć), że typ obiektu pobieranego ze stosu jest właściwy.
Dlatego w wielu językach programowania ze statyczną kontrolą zgodności typów wprowadzono pojęcie klas lub funkcji uogólnionych (ang. generics). Pomysł jest bardzo prosty, chcemy sparametryzować fragment kodu typem przetwarzanych danych, więc dodajemy nowy rodzaj parametrów opisujących typy.
Zanim dokładniej przyjrzymy się rozwiązaniu tego problemu w Javie, warto zaznaczyć, że realizacje klas uogólnionych we współczesnych językach obiektowych dość istotnie różnią się od siebie.<ref name="PO_typy_funkcyjne"> A jeszcze inne podejście do tego problemu można znaleźć np. w realizacji funkcji uogólnionych w językach funkcyjnych.</ref>Rozwiązanie przyjęte w C++ opiera się na bardzo prostej idei (z grubsza rzecz biorąc sprowadza się do rozwijania makr) ma natomiast olbrzymią siłę wyrazu (np. umożliwia makroprogramowanie). Kwestią do dyskusji pozostaje, czy tak duża siła wyrazu tego mechanizmu rzeczywiście jest potrzebna. W Javie zastosowano zupełnie inne rozwiązanie, oparte o ideę wymazywania typów (ang. type erasure), co było wymuszone koniecznością zachowania zgodności z wcześniejszymi wersjami (przede wszystkim chodziło o zgodność z dotychczasową maszyną wirtualną). Zaproponowane rozwiązanie jest zgrabne i wystarczające w większości przypadków, jest ma szereg irytujących ograniczeń, których przyczyny nie można zrozumieć bez odwołania się do zastosowanego mechanizmu wymazywania typów. Wreszcie C# oferuje rozwiązanie bardzo podobne do tego z Javy, jednak dzięki temu, że twórcy nie byli zmuszeni do zachowania zgodności z poprzednimi wersjami maszyny wirtualnej, jest ono bardziej spójne.
Klasy parametryzowane typami
Metody parametryzowane typami
Ograniczenia typów uogólnionych w Javie
<references/>