Kompatybilność wsteczna

Kompatybilność wsteczna jest cechą języka Java, dzięki której aplikacje napisane w starszych jej wersjach można uruchamiać na nowszych wersjach maszyny wirtualnej. Jest to jeden z aspektów, dzięki którym Java zyskała tak dużą popularność i nadal utrzymuje się w czołówce najbardziej popularnych języków programowania.

Jak to działa

Załóżmy, że dołączamy do projektu, który powstał wiele lat temu i pierwotnie był napisany np. w Javie 5. W kolejnych latach pojawiały się nowe wersje Javy, czyli 6, 7, 8, itd., a wraz z nimi usprawnienia języka takie jak możliwość używania typu String w instrukcji switch, instrukcja try with resources, wyrażenia lambda, strumienie, czy metody domyślne w interfejsach. Dzięki kompatybilności wstecznej Javy mamy gwarancję, że kod napisany np. w Javie 1.5 uda się skompilować i uruchomić także z wykorzystaniem Javy 8, czy 10.

Adnotacja @Deprecated

W praktyce nie da się zaprojektować idealnego języka programowania i jego API w taki sposób, żeby wszystkie decyzje podjęte na początku były równie aktualne 20 lat później. Wystarczy spojrzeć na to, że na przestrzeni ostatnich 20-30 lat przeszliśmy z modelu desktopowych aplikacji dystrybuowanych na płytach CD, które trzeba było zainstalować na swoim komputerze, to dystrybucji cyfrowej i rozwiązań chmurowych. Dziś większość aplikacji, z których korzystamy działa tak naprawdę w przeglądarce internetowej. Wyjątkami są programy, które wymagają dużej mocy obliczeniowej naszego komputera. Zaprojektowanie języka programowania w taki sposób, aby dostarczał on rozwiązania pozwalające na tworzenie aplikacji, niezależnie od aktualnych wymagań i trendów jest nie lada wyczynem.

Z tego powodu w języku Java dostępna jest adnotacja @Deprecated, która pozwala wskazać, że wybrany element języka: klasa, metoda, czy konstruktor, nie powinny być już używane, ponieważ zostały zastąpione innym, lepszym rozwiązaniem. Przykładem są np. konstruktory z klas opakowujących. Jeżeli jakiś element został oznaczony jako @Deprecated, to w środowisku programistycznym będzie przekreślony, co łatwo pomaga zidentyfikować potencjalny problem:

@Deprecated(forRemoval=true)

Oznaczenie czegoś adnotacją @Deprecated jasno wskazuje, że z danego elementu nie powinniśmy korzystać, ale jednocześnie ze względu na kompatybilność wsteczną twórcy Javy nie mieli możliwości usunięcia takiego elementu z kodu. Sprawia to pewne problemy, ponieważ wokół języka narasta dług technologiczny i pomimo dodawania nowych funkcjonalności, nadal utrzymywane są rzeczy, które w niektórych przypadkach nigdy nie powinny się pojawić.

Z tego powodu w Javie 9 dodano do adnotacji @Deprecated dodatkowy atrybut forRemoval, który pozwala wskazać, że zaplanowano usunięcie danego elementu w jednej z kolejnych wersji języka. Nie ma gwarancji, czy dany element zniknie w kolejnej wersji, czy może później, ale jest to wskazanie, że używasz czegoś na własne ryzyko. Można to porównać do daty przydatności do spożycia na produktach spożywczych. Jeżeli na jogurcie jest informacja o tym, że najlepiej go spożyć do 15.10.2021, to co prawda jeżeli zjesz go 16.10 to raczej nic Ci się nie stanie, ale gdyby jednak Ci zaszkodził, to producent Cię przed tym ostrzegał.

Atrybut forRemoval pozwala więc zerwać z pełną kompatybilnością wsteczną, ale nie jest to wada, tylko zaleta. Elementy języka nie są usuwane bez zastanowienia z wersji na wersję, tylko najpierw otrzymujemy ostrzeżenie i mamy czas, żeby się odpowiednio przygotować do zmian. Dodatkowo API zostaje oczyszczone z takich rzeczy, które z perspektywy czasu można uznać za błędne.

Przykład - Typy generyczne

W starszych wersjach języka istniały kolekcje w ramach Collections Framework, ale pozwalały one operować wyłącznie na typie Object, co wymuszało późniejsze rzutowanie obiektów na odpowiedni typ, np.:

Java 1.4:

List numbers = new ArrayList();
numbers.add(5);
numbers.add(10);
int sum = ((Integer)numbers.get(0)).intValue() + ((Integer)numbers.get(1)).intValue();

W Javie 1.5 wprowadzono typy generyczne, które pozwalają określić dodatkowe parametry na etapie definiowania obiektów. Oprócz tego pojawił się autoboxing i autounboxing i ten sam kod można zapisać już tak:

List<Integer> numbers = new ArrayList<Integer>();
numbers.add(5);
numbers.add(10);
int sum = numbers.get(0) + numbers.get(1);

Wprowadzenie typów generycznych nie oznaczało jednak, że kod, który był zapisany wcześniej, przestaje działać, wręcz przeciwnie, jest on nadal poprawny, ale w naturalny sposób przestaje być używany przez programistów, którzy wybiorą wygodniejszy zapis.

Dalszą ewolucją był operator diamentu z Javy 7, dzięki któremu nie musimy powtarzać już typu generycznego przy definiowaniu obiektu. Zamiast tego zostaje on wywnioskowany z typu podanego przy deklaracji:

List<Integer> numbers = new ArrayList<>();
//...

Tutaj po raz kolejny kod, który napisaliśmy w Javie 1.4, albo 1.5 skompiluje się z wykorzystaniem kompilatora Javy 7, ponieważ kompatybilność wsteczna była cały czas zachowana.

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.