Java – obiektowy, oparty na klasach, statycznie i silnie typowany język programowania ogólnego zastosowania, wspierający programowanie współbieżne. Podstawą języka jest model obiektowy, jednak jego kolejne wersje wprowadziły również mechanizmy umożliwiające stosowanie elementów programowania funkcyjnego, w szczególności wyrażenia lambda, referencje do metod i interfejsy funkcyjne.
Kod źródłowy Javy jest zwykle kompilowany do niezależnego od konkretnego procesora kodu bajtowego, wykonywanego przez wirtualną maszynę Javy (JVM). JVM stanowi podstawę niezależności platformy Java od sprzętu i systemu operacyjnego, dzięki czemu ten sam skompilowany kod może być wykonywany na różnych platformach posiadających zgodne środowisko uruchomieniowe. Współczesne implementacje JVM mogą podczas działania programu generować i dynamicznie optymalizować kod maszynowy. Alternatywny sposób wdrażania aplikacji oferuje GraalVM Native Image, wykorzystujący kompilację AOT do utworzenia natywnego pliku wykonywalnego dla określonego systemu operacyjnego i architektury, bez uruchamiania aplikacji na klasycznej JVM.
Język i platforma Java są rozwijane poprzez kolejne wydania Java SE i JDK. W nowszych wersjach języka wprowadzono m.in. rekordy, klasy zapieczętowane (sealed classes), dopasowywanie wzorców (pattern matching), możliwość używania instrukcji switch jako wyrażenia zwracającego wartość, bloki tekstowe oraz uproszczoną składnię prostych programów. Platformę rozszerzono również m.in. o wątki wirtualne (virtual threads), lekkie wątki zarządzane przez środowisko JDK, umożliwiające obsługę bardzo dużej liczby współbieżnych zadań, oraz Foreign Function and Memory API, zapewniające dostęp do funkcji i pamięci spoza środowiska Java.
Historia
Początki Javy wiążą się z projektem Green, rozpoczętym w Sun Microsystems w 1991 roku przez Jamesa Goslinga, Patricka Naughtona i Mike’a Sheridana. Celem projektu było opracowanie oprogramowania dla nowej generacji urządzeń elektroniki użytkowej, które mogły wykorzystywać różne architektury sprzętowe. Zespół potrzebował rozwiązania przenośnego i niezależnego od konkretnego procesora.
W ramach projektu James Gosling zaprojektował nowy język programowania, nazwany początkowo Oak. W jego dalszym rozwoju znaczący udział mieli Ed Frank, Patrick Naughton, Jonathan Payne i Chris Warth. W kolejnych latach język został ukierunkowany na zastosowania internetowe, przemianowany na Java i znacząco zmodyfikowany. Jego ostateczną wczesną postać zdefiniowali James Gosling, Bill Joy, Guy Steele, Richard Tuck, Frank Yellin i Arthur van Hoff, przy udziale Grahama Hamiltona, Tima Lindholma i innych współpracowników.
Java została publicznie zaprezentowana przez Sun Microsystems w 1995 roku. Jednym z podstawowych założeń projektu była możliwość wykonywania tego samego programu na różnych platformach sprzętowych i systemowych. Koncepcję tę wyrażało hasło Write Once, Run Anywhere. Pierwszą wersję JDK 1.0 wydano 23 stycznia 1996 roku.
W kolejnych wersjach rozwijano zarówno sam język, jak i bibliotekę standardową oraz wirtualną maszynę Javy. Java 1.2 została wydana w grudniu 1998 roku. Od wersji 1.2 platformę określano jako Java 2 Platform, rozwijaną następnie w trzech głównych edycjach: Java 2 Platform, Standard Edition (J2SE), Java 2 Platform, Enterprise Edition (J2EE) oraz Java 2 Platform, Micro Edition (J2ME). J2SE była przeznaczona przede wszystkim dla komputerów osobistych i stacji roboczych, J2EE dla zastosowań korporacyjnych, a J2ME dla urządzeń o ograniczonych zasobach. W 2005 roku nazwę J2ME zmieniono na Java ME, natomiast wraz z wersjami platform wydanymi na początku 2006 roku nazwy J2SE i J2EE zastąpiono odpowiednio przez Java SE i Java EE.
W 2006 roku Sun rozpoczął udostępnianie implementacji JDK jako otwartego oprogramowania, a w 2007 roku opublikowano większość kodu projektu OpenJDK na licencji GPLv2 z wyjątkiem Classpath.
W styczniu 2010 roku Oracle Corporation zakończył przejęcie Sun Microsystems i stał się zarządcą należących do tej firmy technologii Java.
Od 2018 roku kolejne główne wydania JDK ukazują się w regularnym, półrocznym cyklu.
Charakterystyka języka
Java jest językiem opartym na klasach i statycznym typowaniu. Specyfikacja języka określa go również jako język silnie typowany, co oznacza, że każda zmienna i każde wyrażenie mają określony typ, a wiele niezgodności typów jest wykrywanych podczas kompilacji.
Model obiektowy i system typów
Java rozróżnia osiem typów prostych: boolean, byte, short, int, long, char, float i double, oraz typy referencyjne, do których należą klasy, interfejsy i tablice.
Obiekty są instancjami klas. Klasa może określać stan obiektu za pomocą pól oraz jego zachowanie za pomocą metod. Język obsługuje m.in. enkapsulację, polimorfizm, klasy abstrakcyjne i interfejsy.
Wszystkie klasy, z wyjątkiem Object, mają bezpośrednią klasę nadrzędną. Java nie umożliwia wielokrotnego dziedziczenia klas, natomiast jedna klasa może implementować wiele interfejsów. Od Java 8 interfejsy mogą zawierać również metody domyślne posiadające implementację.
Wszystkie zwykłe klasy dziedziczą ostatecznie po klasie java.lang.Object, która definiuje podstawowe operacje dostępne dla obiektów, m.in. equals(), hashCode(), toString() i getClass().
W kolejnych wersjach Javy model typów rozszerzano m.in. o klasy rekordowe (records), przeznaczone przede wszystkim do reprezentowania agregatów danych, oraz klasy i interfejsy zapieczętowane (sealed), pozwalające ograniczyć zbiór klas, które mogą po nich dziedziczyć. Rekordy stały się stałą częścią języka w Java 16, a klasy zapieczętowane w Java 17.
Typy generyczne
Java obsługuje typy generyczne, umożliwiające definiowanie klas, interfejsów i metod parametryzowanych typem. Mechanizm ten pozwala m.in. określać typ elementów kolekcji i wykrywać część błędów typów podczas kompilacji zamiast podczas wykonywania programu.
Implementacja generyków w Javie wykorzystuje mechanizm wymazywania typów (type erasure). Większość informacji dotyczących argumentów typów generycznych nie jest zatem reprezentowana w kodzie wykonywalnym w taki sam sposób jak zwykłe typy klas.
Elementy programowania funkcyjnego
Podstawowym modelem Javy pozostaje programowanie obiektowe, jednak od Java 8 język zawiera mechanizmy pozwalające stosować elementy programowania funkcyjnego. Wprowadzono wówczas wyrażenia lambda, referencje do metod oraz interfejsy funkcyjne.
Biblioteka standardowa udostępnia m.in. API strumieni (Stream API), pozwalające wykonywać operacje takie jak filtrowanie, przekształcanie i agregowanie sekwencji danych przy użyciu stylu deklaratywnego.
Dopasowywanie wzorców i rozwój składni
W nowszych wersjach języka rozwijany jest mechanizm dopasowywania wzorców (pattern matching). Java 16 wprowadziła jego wykorzystanie z operatorem instanceof, natomiast Java 21 wprowadziła wzorce rekordów oraz dopasowywanie wzorców w konstrukcji switch.
Od Java 14 konstrukcja switch może być również używana jako wyrażenie zwracające wartość. W kolejnych wydaniach wprowadzono ponadto m.in. bloki tekstowe, lokalne wnioskowanie typów przez var, nienazwane zmienne i wzorce oraz uproszczony sposób definiowania prostych programów.
Obsługa wyjątków
Java wykorzystuje wyjątki do obsługi sytuacji powodujących przerwanie zwykłego toku wykonywania programu. Każdy wyjątek jest reprezentowany przez obiekt klasy java.lang.Throwable lub jednej z jej podklas, w tym Exception i Error.
Język rozróżnia wyjątki kontrolowane (checked exceptions) oraz niekontrolowane. W przypadku wyjątków kontrolowanych kompilator wymaga ich obsłużenia albo zadeklarowania możliwości ich zgłoszenia w sygnaturze metody. Wymóg ten nie dotyczy klas dziedziczących po RuntimeException i Error.
Współbieżność
Mechanizmy programowania współbieżnego należą do podstawowych elementów platformy Java. Język definiuje m.in. synchronizację za pomocą słowa kluczowego synchronized, natomiast biblioteka standardowa zawiera pakiet java.util.concurrent oraz powiązane podpakiety, obejmujące m.in. pule wątków, kolejki współbieżne, blokady, semafory oraz typy atomowe.
Od Java 21 standardową częścią platformy są wątki wirtualne (virtual threads). Są to lekkie wątki zarządzane przez środowisko uruchomieniowe, których celem jest umożliwienie tworzenia aplikacji obsługujących bardzo dużą liczbę współbieżnych zadań przy zachowaniu modelu programowania opartego na wątkach.
Pakiety i moduły
Klasy i interfejsy są grupowane w pakiety, które tworzą przestrzenie nazw. Pełna nazwa klasy obejmuje nazwę pakietu, np. java.util.ArrayList. Nazwy pakietów mogą mieć strukturę wieloczłonową, lecz nie tworzą hierarchii dziedziczenia. Przykładowo java.util i java.util.concurrent są z punktu widzenia języka odrębnymi pakietami.
Od Java 9 platforma wykorzystuje także Java Platform Module System. Moduł grupuje pakiety i może jawnie określać zależności od innych modułów oraz pakiety udostępniane na zewnątrz. Standardowa biblioteka Java została podzielona na moduły, z których podstawowym jest java.base.
Kompilacja i wykonywanie programów
Kod bajtowy i JVM
Kod źródłowy Javy jest zazwyczaj przechowywany w plikach z rozszerzeniem .java. Standardowy kompilator javac przekształca go w pliki klas .class zawierające instrukcje kodu bajtowego.
Kod ten jest przeznaczony dla wirtualnej maszyny Javy (JVM), zdefiniowanej przez osobną specyfikację. JVM jest maszyną abstrakcyjną i nie określa jednej konkretnej implementacji. Specyfikacja definiuje m.in. format plików klas, zestaw instrukcji, obszary pamięci, mechanizmy ładowania i inicjalizacji klas oraz sposób wykonywania instrukcji.
Przed wykonaniem kodu JVM może przeprowadzić weryfikację plików klas. Weryfikator sprawdza m.in. poprawność typów i strukturę instrukcji, ograniczając możliwość wykonania niepoprawnego kodu bajtowego.
Niezależność od platformy
Kod bajtowy JVM nie odpowiada bezpośrednio zestawowi instrukcji jednego konkretnego procesora. Pozwala to wykonywać ten sam plik klas na różnych architekturach i systemach operacyjnych posiadających zgodną implementację JVM.
Przenośność nie oznacza całkowitej niezależności każdego programu od środowiska. Aplikacja może korzystać m.in. z natywnych bibliotek, właściwości systemu plików, urządzeń lub interfejsów systemu operacyjnego, przez co jej działanie może zależeć od konkretnej platformy.
Kompilacja JIT
Współczesne implementacje JVM nie ograniczają się do interpretowania kodu bajtowego. HotSpot, używany w OpenJDK, wykorzystuje kompilację JIT, w której kod jest kompilowany do instrukcji maszynowych w trakcie działania programu.
HotSpot stosuje profilowanie wykonywanego programu i może kompilować częściej wykonywane fragmenty z różnymi poziomami optymalizacji. Domyślnie wykorzystywana jest kompilacja warstwowa (tiered compilation), łącząca szybkie rozpoczęcie wykonywania z późniejszą optymalizacją często używanego kodu.
Kompilacja AOT i GraalVM Native Image
Alternatywnym modelem wykonywania programów Java jest kompilacja AOT. Jednym z rozwiązań stosujących ten model jest GraalVM Native Image, które analizuje aplikację i jej zależności podczas budowania, a następnie kompiluje wymagany kod do natywnego pliku wykonywalnego przeznaczonego dla konkretnego systemu operacyjnego i architektury.
Utworzony w ten sposób program nie jest wykonywany przez klasyczną JVM i zawiera potrzebne komponenty środowiska uruchomieniowego. Native Image pozwala m.in. ograniczyć czas uruchamiania i zużycie pamięci, lecz analiza wykonywana podczas budowania wymaga znajomości kodu osiągalnego w czasie działania, co ma znaczenie m.in. dla aplikacji intensywnie korzystających z refleksji lub dynamicznego ładowania klas.
Zarządzanie pamięcią
Java wykorzystuje automatyczne odśmiecanie pamięci. Programista tworzy obiekty, natomiast pamięć zajmowana przez obiekty, do których nie można już dotrzeć z aktywnej części programu, może zostać automatycznie odzyskana przez garbage collector.