Wyjątek NullPointerException jest jedną z najczęstszych przyczyn błędów programów Java.
W najprostszym przypadku kompilator może bezpośrednio ostrzec w przypadku napotkania kodu podobnego do poniższego:
Object o = null;
String s = o.toString();
W przypadku rozgałęzień, pętli i zgłaszania wyjątków konieczne jest użycie analizy przepływu, aby móc określić, czy dereferowana zmienna ma przypisaną wartość NULL w niektórych lub wszystkich ścieżkach programu.
Z powodu swojej złożoności analiza przepływu jest najskuteczniejsza na małych fragmentach kodu. Analizowanie jednej metody na raz pozwala utrzymać dobrą wydajność środowiska - analiza całego systemu nie jest obsługiwana przez kompilator Java środowiska Eclipse. Korzyści są następujące: analiza jest szybka i może być wykonywana przyrostowo, dzięki czemu kompilator może ostrzegać bezpośrednio w czasie pisania. Wady: w ramach analizy nie jest określane, czy wartości NULL przepływają między metodami (jako parametry i zwracane wartości).
Teraz zostaną użyte adnotacje wartości NULL.
Przez określenie parametru metody @NonNull kompilator otrzymuje instrukcje,
że wartość NULL na tej pozycji ma nie występować.
String capitalize(@NonNull String in) {
return in.toUpperCase(); // sprawdzanie wartości NULL nie jest wymagane
}
void caller(String s) {
if (s != null)
System.out.println(capitalize(s)); // wcześniejsze sprawdzenie wartości NULL jest wymagane
}
Zasady w projekcie opartym na kontrakcie są następujące:
capitalize ma
gwarancję, że argument in nie będzie miał wartości NULL,
więc wyłuskanie bez sprawdzenia wartości NULL jest tutaj poprawne.
Podobnie wygląda sytuacja z wartościami zwracanymi metody:
@NonNull String getString(String maybeString) {
if (maybeString != null)
return maybeString; // powyżej sprawdzenie wartości NULL jest wymagane
else
return "<n/a>";
}
void caller(String s) {
System.out.println(getString(s).toUpperCase()); // sprawdzenie wartości NULL nie jest wymagane
}
Kompilator Java środowiska Eclipse może zostać skonfigurowany do używania trzech różnych typów adnotacji rozszerzonej analizy wartości NULL (domyślnie są one wyłączone):
@NonNull: wartość NULL jest niepoprawna,@Nullable: wartość NULL jest dopuszczalna i należy jej oczekiwać,@NonNullByDefault: typy w sygnaturze metody
bez adnotacji wartości NULL są traktowane tak, jakby określały niepoprawną wartość NULL.Adnotacje @NonNull i @Nullable są obsługiwane w następujących miejscach:
Adnotacja @NonNullByDefault jest obsługiwana w następujących miejscach:
package-info.java) - dotyczy wszystkich typów w pakiecie.
Można również
konfigurować rzeczywiste nazwy kwalifikowane tych adnotacji, ale domyślnie są używane powyższe wartości (pochodzące z pakietu org.eclipse.jdt.annotation).
Podczas używania adnotacji o wartości NULL innych firm należy się upewnić, że
zostały one poprawnie zdefiniowane przy użyciu co najmniej adnotacji meta
@Target, ponieważ w przeciwnym razie kompilator nie odróżni
adnotacji deklaracji (Java 5) od adnotacji typu (Java 8).
Ze środowiskiem Eclipse dostarczany jest plik JAR z domyślnymi adnotacjami wartości NULL, umieszczony w położeniu eclipse/plugins/org.eclipse.jdt.annotation_*.jar. Plik ten musi się znajdować w ścieżce budowania podczas kompilacji, ale nie jest potrzebny podczas wykonywania (więc nie trzeba go dostarczać użytkownikom skompilowanego kodu).
Począwszy od środowiska Eclipse Luna istnieją dwie wersje pliku JAR: jedna z adnotacjami deklaracji do użycia w języku Java 7 lub jego starszej wersji (wersja 1.1.x) i druga z adnotacjami typu wartości NULL do użycia w języku Java 8 (wersja 2.0.x).
Dla zwykłych projektów Java dostępna jest szybka poprawka nierozstrzygniętego odwołania do adnotacji @NonNull, @Nullable lub @NonNullByDefault
polegająca na dodaniu odpowiedniej wersji pliku JAR do ścieżki budowania:

Dla pakunków OSGi i wtyczek należy dodać jedną z następujących pozycji do pliku MANIFEST.MF:
Require-Bundle: ..., org.eclipse.jdt.annotation;bundle-version="[1.1.0,2.0.0)";resolution:=optional
Require-Bundle: ..., org.eclipse.jdt.annotation;bundle-version="[2.0.0,3.0.0)";resolution:=optional
Szczegółowe omówienie tego tematu zawiera odpowiednia sekcja związana ze zgodnością.
Wyjaśniono już, że adnotacje wartości NULL dodają więcej informacji do programu Java (które z kolei mogą zostać użyte przez kompilator do zwracania lepszych ostrzeżeń). Co jednak dokładnie powinny mówić te adnotacje? Patrząc pragmatycznie, adnotacje wartości NULL mogą działać na trzech poziomach:
W przypadku poziomu (1) możesz od razu rozpocząć korzystanie z adnotacji wartości NULL bez czytania dalszej części tematu, ale w tej sytuacji nie należy oczekiwać istotnego ulepszenia kodu. Dwa pozostałe poziomy wymagają dalszych wyjaśnień.
Pierwszą cechą korzystania z adnotacji wartości NULL na potrzeby specyfikacji interfejsu API w projekcie według kontraktu
jest to, że sygnatury wszystkich metod interfejsu API są w pełni adnotowane, tj.
z wyjątkiem typów podstawowych, takich jak int, każdy parametr i każda wartość zwracana
metody powinna być oznaczona jako @NonNull lub @Nullable.
Ponieważ oznaczałoby to konieczność wstawiania bardzo wielu adnotacji wartości NULL, warto jest wiedzieć, że
w dobrze zaprojektowanym kodzie (szczególnie w metodach interfejsu API), adnotacja @NonNull występuje zdecydowanie częściej
niż adnotacja @Nullable. Pozwala to ograniczyć liczbę adnotacji
przez zadeklarowanie adnotacji @NonNull jako domyślnej za pomocą adnotacji @NonNullByDefault
na poziomie pakietu.
Istotna różnica między adnotacją @Nullable i pominięciem adnotacji wartości NULL:
ta adnotacja określa jawnie, że wartość NULL jest dozwolona i należy jej oczekiwać.
Brak adnotacji to po prostu określenie, że nie wiadomo, czego się spodziewać.
Do tej pory typową sytuacją bywało, że obie strony (wywołujący i wywoływany) nadmiarowo sprawdzały wartości NULL,
i że czasem jedna strona przyjmowała niepoprawne założenia na temat wykonania sprawdzenia przez drugą stronę.
To właśnie podstawowa przyczyna występowania wyjątków NullPointerException.
Bez adnotacji kompilator nie może udzielić konkretnej porady, a dzięki adnotacji
@Nullable każde niesprawdzone wyłuskanie zostanie oflagowane.
Dysponując tymi informacjami, można bezpośrednio odwzorować wszystkie adnotacje parametrów na warunki początkowe i interpretować adnotacje wartości zwracanych jako warunki końcowe metody.
W programowaniu obiektowym pojęcie projektu według kontraktu musi objąć jeszcze jeden wymiar:
typy podrzędne i przesłanianie (termin „przesłanianie” będzie używany w kontekście adnotacji
@Override środowiska Java 6: chodzi o metody przesłaniające lub implementujące inne metody
z nadtypu). Klient wywołujący następującą metodę:
@NonNull String checkedString(@Nullable String in)
powinien móc założyć, że wszystkie implementacje tej metody są zgodne z kontraktem.
Dlatego też, gdy deklaracja metody znajduje się w interfejsie I1,
należy wykluczyć ryzyko, że dowolna klasa Cn implementująca interfejs I1 udostępni
niezgodną implementację. W szczególności niedozwolone jest, aby dowolna klasa Cn próbowała
przesłaniać tę metodę implementacją deklarującą parametr jako @NonNull.
Jeśli takie zachowanie byłoby dopuszczalne, moduł klienta zaprogramowany zgodnie z interfejsem I1 mógłby w sposób
niedozwolony przekazać wartość NULL jako argument, zaś implementacja przyjęłaby założenie braku wartości NULL -
niesprawdzona dereferencja w implementacji metody spowodowałaby błąd w czasie wykonywania.
Z tego powodu specyfikacja parametru @Nullable obowiązuje we wszystkich przesłonięciach,
określając wartość NULL jako oczekiwaną i dopuszczalną wartość.
Podobnie specyfikacja zwracanej wartości @NonNull obowiązuje we wszystkich przesłonięciach,
zapewniając że wartość NULL nigdy nie zostanie zwrócona.
Kompilator musi więc sprawdzić, czy przesłonięcia nie dodają adnotacji parametru @NonNull
(lub adnotacji wartości zwracanej @Nullable) nieistniejącej w nadtypie.
Co ciekawe, odwrotne ponowne definiowanie jest dozwolone: dodawanie adnotacji parametru @Nullable
lub adnotacji wartości zwracanej @NonNull (można to uważać za „ulepszenia” metody -
akceptuje ona wówczas więcej wartości i zwraca bardziej konkretną wartość).
Wymuszenie powtarzania adnotacji wartości NULL przez podklasy we wszystkich metodach przesłaniających pozwala zrozumieć kontrakt wartości NULL każdej metody bez konieczności przeszukiwania hierarchii dziedziczenia. Jednak w sytuacjach, w których hierarchia dziedziczenia zawiera kod różnego pochodzenia, dodanie adnotacji wartości NULL do wszystkich klas jednocześnie może okazać się niemożliwe. W takich przypadkach można ustawić kompilator tak, aby traktował metody z brakującymi adnotacjami wartości NULL w taki sposób, jakby adnotacje z przesłoniętej metody były dziedziczone. Tę opcję można włączyć za pomocą opcji kompilatora Dziedzicz adnotacje wartości NULL. Metoda może przesłonić dwie metody z różnymi kontraktami wartości NULL. Ponadto do metody można zastosować domyślną dopuszczalność wartości NULL, która powoduje konflikt z dziedziczoną adnotacją wartości NULL. Te przypadki są oznaczane jako błąd i metoda przesłaniająca musi użyć jawnej adnotacji wartości NULL w celu rozwiązania konfliktu.
@NonNull na wartość nieokreśloną?
Jeśli
dziedziczenie
adnotacji wartości NULL nie zostało włączone, istnieje jedna
konkretna sytuacja, która jest bezpieczna z punktu widzenia teorii typu,
chociaż nadal może wskazywać na problem: odebranie metody nadrzędnej, która
deklaruje parametr jako @NonNull i metody przesłaniającej, która
nie ogranicza odpowiedniego parametru (ani przez jawną adnotację wartości NULL,
ani przez stosowaną adnotację @NonNullByDefault).
Jest to bezpieczne, ponieważ klienty, które wykryją deklarację
nadrzędną, zostaną zmuszone do unikania wartości NULL, natomiast
implementacja przesłaniająca nie może skorzystać z tej gwarancji ze względu na
brak specyfikacji w tej konkretnej metodzie.
Może to jednak prowadzić do błędnych założeń, ponieważ stosowanie deklaracji w typie nadrzędnym również do wszystkich przesłonięć może być zamierzone.
Z tego powodu kompilator udostępnia opcję Parametr @NonNull nie ma adnotacji w metodzie przesłaniającej:
NULL ma
rzeczywiście być akceptowana, zaleca się dodanie adnotacji
@Nullable w celu przesłonięcia adnotacji @NonNull
metody nadrzędnej.
Powyższe zachowanie powoduje pewną trudność w przypadku, gdy kod z adnotacjami jest pisany jako podtyp
„wcześniejszego” typu (tj. typu bez adnotacji), który może np. pochodzić z biblioteki innej firmy i nie można go zmienić.
Uważne przeczytanie powyższej sekcji pozwala zauważyć, że nie można przyjąć
przesłonięcia „wcześniejszej” metody przez metodę z parametrem @NonNull
(dlatego że klienty korzystające z nadtypu nie widzą zobowiązania @NonNull).
W takiej sytuacji konieczna jest rezygnacja ze stosowania adnotacji wartości NULL (planowane jest dodanie obsługi adnotacji w takich przypadkach, ale nie jest to jeszcze gwarantowane i nie wiadomo, kiedy będzie dostępne).
Sytuacja komplikuje się, gdy podtyp „wcześniejszego” typu znajduje się w pakiecie, dla którego określono adnotację
@NonNullByDefault. Teraz typ z nadtypem bez adnotacji
musi oznaczyć wszystkie parametry w przesłanianych metodach jako @Nullable:
nie jest nawet dozwolone pomijanie adnotacji parametrów, ponieważ takie pominięcie będzie interpretowane jako parametr typu
@NonNull, co nie będzie dozwolone w takim miejscu.
Dlatego też kompilator Java środowiska Eclipse obsługuje anulowanie
domyślnego określenia wartości NULL. Można to zrobić, dodając do metody lub typu adnotację @NonNullByDefault(false)
co spowoduje anulowanie odpowiedniej wartości domyślnej elementu i interpretowanie parametrów bez adnotacji
jako nieokreślonych. Teraz typ podrzędny jest poprawny bez stosowania nadmiarowych adnotacji @Nullable:
class LegacyClass {
String enhance (String in) { // na klientach nie jest wymuszone przekazywanie wartości innej niż NULL.
return in.toUpperCase();
}
}
@NonNullByDefault
class MyClass extends LegacyClass {
// (...)metody z wartością domyślną @NonNull(...)
@Override
@NonNullByDefault(false)
String enhance(String in) { // byłoby niepoprawne, gdyby obowiązywała tu adnotacja @NonNullByDefault
return super.enhance(in);
}
}
Adnotacje wartości NULL działają najlepiej po zastosowaniu w sygnaturach metod (zmienne lokalne najczęściej nawet ich nie potrzebują, ale mogą korzystać z adnotacji wartości NULL w celu łączenia kodu z adnotacjami i wcześniejszego kodu). W takich przypadkach adnotacje wartości NULL łączą porcje analizy procedur wewnętrznych w celu uzyskania instrukcji dotyczących przepływów danych globalnych. Począwszy od środowiska Eclipse Kepler adnotacje wartości NULL mogą być również stosowane do pól, ale tutaj sytuacja jest trochę inna.
Oznaczenie pola adnotacją @NonNull oznacza oczywiście,
że każde przypisanie do pola musi udostępniać wartość, o której wiadomo, że nie
jest wartością NULL.
Ponadto kompilator musi mieć możliwość zweryfikowania, że
do pola o wartości innej niż NULL nie można uzyskać dostępu w niezainicjowanym
stanie (w którym pole ma nadal wartość NULL).
Jeśli można zweryfikować, że każdy konstruktor jest zgodny z tą regułą
(podobnie pole statyczne musi mieć inicjator), program zyskuje na
bezpieczeństwie, ponieważ wyłuskiwanie pola nigdy nie spowoduje wystąpienia
wyjątku NullPointerException.
Sytuacja jest bardziej złożona w przypadku pola oznaczonego adnotacją
@Nullable.
Takie pole należy zawsze uważać za niebezpieczne i przestrzegać następującego
zalecanego sposobu pracy z polami z dopuszczalną wartością NULL: należy
zawsze przypisywać wartość do zmiennej lokalnej przed rozpoczęciem z nią
pracy.
Stosując zmienną lokalną na podstawie analizy przepływu można dokładnie
określić, czy wyłuskanie jest wystarczająco chronione przez sprawdzenie
wartości NULL. Przestrzeganie tej ogólnej reguły podczas pracy z polami o
dopuszczalnej wartości NULL nie powoduje problemów.
Sytuacja staje się bardziej skomplikowana, gdy kod bezpośrednio wyłuskuje wartość pola z dopuszczalną wartością NULL. Problem polega na tym, że działania sprawdzania wartości NULL, które mogą zostać wykonane przez kod przed wyłuskaniem, mogą zostać łatwo unieważnione przez następujące czynniki:
Łatwo można zauważyć, że bez jednoczesnej analizy synchronizacji wątków (wykraczającej poza możliwości kompilatora) sprawdzenie wartości NULL dotyczące pola z dopuszczalną wartością NULL nigdy nie zagwarantuje 100% bezpieczeństwa dla wykonywanego następnie wyłuskania. Jeśli więc współbieżny dostęp do pola z dopuszczalną wartością NULL jest możliwy, wartość pola nigdy nie powinna być wyłuskiwana w sposób bezpośredni. Zamiast tego należy zawsze używać zmiennej lokalnej. Nawet jeśli nie występuje żadna współbieżność, pozostałe problemy stanowią wyzwanie dla kompletnej analizy, która jest trudniejsza niż zadania zazwyczaj obsługiwane przez kompilator.
Ponieważ kompilator nie może w pełni przeanalizować skutków używania aliasów, efektów ubocznych i współbieżności, kompilator Eclipse nie wykonuje żadnej analizy przepływu dla pól (poza analizą dotyczącą ich inicjowania). Ponieważ wielu programistów uważa to ograniczenie za zbyt surowe z powodu wymogu stosowania zmiennych lokalnych, podczas gdy jednocześnie uważają oni, że ich kod powinien być bezpieczny, jako możliwy kompromis wprowadzono nową opcję:
Kompilator może zostać skonfigurowany na potrzeby wykonywania analizy składniowej. Spowoduje to wykrycie najbardziej oczywistych wzorców, np.:
@Nullable Object f;
void printChecked() {
if (this.f != null)
System.out.println(this.f.toString());
}
Po włączeniu tej opcji powyższy kod nie zostanie oznaczony przez kompilator. Należy pamiętać, że ta analiza składniowa nie jest pod żadnym względem inteligentna. Jeśli między sprawdzeniem a wyłuskaniem wystąpi jakikolwiek kod, kompilator „zapomni” o informacjach na temat poprzedniego sprawdzenia wartości NULL, nawet nie próbując sprawdzić, czy kod pośredni może być nieszkodliwy przynajmniej według niektórych kryteriów. Dlatego należy pamiętać, że zawsze, gdy kompilator oznaczy wyłuskanie pola z dopuszczalną wartością NULL jako niebezpieczne (chociaż można łatwo zauważyć, że wartość NULL nie powinna w nim występować), należy przebudować kod w taki sposób, aby był ściśle zgodny z podanym powyżej rozpoznawanym wzorcem lub użyć zmiennej lokalnej w celu skorzystania z zaawansowanej analizy przepływu, której poziom nigdy nie zostanie osiągnięty przez analizę składniową.
Korzystanie z adnotacji wartości NULL w opisanym stylu projektu według kontraktu pomaga zwiększyć jakość kodu Java na kilka sposobów: na poziomie interfejsu między metodami jest określane jawnie, które parametry i wartości zwracane zezwalają na wartość NULL, a które nie. Pozwala to kompilatorowi rozumieć decyzje projektowe, które są podejmowane przez programistów.
Oprócz tego w oparciu o tę specyfikację interfejsu analiza przepływu wewnątrz procedury może korzystać z dostępnych informacji, aby zwrócić bardziej precyzyjne błędy i ostrzeżenia. Bez adnotacji wartości przepływające do i z metody nie miałby znanej dopuszczalności wartości NULL i analiza przepływu nie mogłaby informować o ich użyciu. Z adnotacjami wartości NULL na poziomie interfejsu API wartość NULL w większości wartości jest znana, więc kompilator wykryje znacznie więcej sytuacji, w których występują wyjątki pustego wskaźnika. Należy jednak pamiętać, że wciąż istnieją pewne luki - gdy w ramach analizy pojawią się nieokreślone wartości, nie będzie można z całą pewnością określić, czy w czasie wykonania wystąpią wyjątki pustego wskaźnika.
Obsługę adnotacji o wartości NULL zaprojektowano tak, aby była zgodna z przyszłymi rozszerzeniami. Rozszerzenie jest częścią języka Java, podobnie jak adnotacje typu (JSR 308), które wprowadzono w języku Java 8. Środowisko JDT obsługuje użycie nowego pojęcia dla adnotacji typu o wartości NULL.
Szczegóły semantyczne analizy wartości NULL opartej na adnotacjach są prezentowane w tym miejscu przez wyjaśnienie reguł sprawdzanych przez kompilator i komunikatów zwracanych po naruszeniu reguły.
Na odpowiedniej stronie preferencji poszczególne reguły sprawdzane przez kompilator są pogrupowane w następujących nagłówkach:
Naruszenie specyfikacji to każda sytuacja, w której adnotacja wartości NULL
jest naruszana przez rzeczywistą implementację. Typowe sytuacje to określanie
wartości (lokalnej, argumentu lub zwracanej) jako @NonNull i
udostępnienie w implementacji wartości z możliwą wartością NULL. Wyrażenie „możliwa wartość NULL” oznacza,
że albo jest statystycznie możliwe uzyskanie wartości NULL, albo że jest ona zadeklarowana
z adnotacją @Nullable.
Oprócz tego w ramach tej grupy są zawarte reguły dotyczące przesłaniania metod opisane
powyżej.
Na przykład metoda nadrzędna określa, że wartość NULL jest poprawnym argumentem,
a przesłoniona metoda znosi to określenie, przyjmując że wartość NULL nie jest poprawnym argumentem.
Jak wspomniano, nawet określenie argumentu bez adnotacji jako argumentu @NonNull
jest naruszeniem specyfikacji, ponieważ wprowadza to kontrakt wiążący klienta
(tj. aby nie przekazywał wartości NULL), ale klient korzystający z nadtypu będzie nieświadomy istnienia tego kontraktu i nie będzie wiedział, czego się od niego oczekuje.
Pełna lista sytuacji uważanych za naruszenia specyfikacji jest dostępna
tutaj.
Ważne jest, aby błędy w tej grupie nigdy nie były ignorowane,
ponieważ w przeciwnym razie cała analiza wartości NULL będzie wykonywana w oparciu o nieprawdziwe przesłanki.
W szczególności gdy kompilator otrzymuje adnotację @NonNull, przyjmuje on za pewnik,
że wartość NULL nie wystąpi w czasie wykonania.
Są to reguły naruszeń specyfikacji, które sprawiają, że analiza działa poprawnie.
Z tego powodu jest zdecydowanie zalecane, aby ten rodzaj problemów był skonfigurowany jako błędy.
Reguły w tej grupie również sprawdzają zgodność ze specyfikacją wartości NULL.
Tutaj jednak obsługiwane są wartości, które nie są zadeklarowane jako @Nullable
(ani nie jest deklarowana sama wartość NULL), ale wartości w ramach analizy przepływu wewnątrz procedury
jest ustalane, że wartości NULL mogą wystąpić w pewnych ścieżkach wykonania.
Wynika to z ustalenia przez kompilator (za pomocą analizy przepływu), czy zmienne lokalne bez adnotacji mogą zawierać wartości NULL. Przyjmując założenie, że taka analiza jest dokładna, problemy wykryte przez te reguły mają taką samą istotność jak bezpośrednie naruszenia specyfikacji NULL. Z tego powodu ponownie jest zdecydowanie zalecane, aby te problemy były skonfigurowane jako błędy, a komunikaty nie były ignorowane.
Tworzenie osobnej grupy dla tych problemów ma dwa cele: udokumentowanie, że dany problem został wykryty przez analizę przepływu, oraz uwzględnienie faktu, że analiza przepływu potencjalnie mogła być błędna (z powodu błędu w implementacji). W przypadku potwierdzenia błędu w implementacji można w drodze wyjątku zignorować błąd tego rodzaju.
Z uwagi na naturę analiz statystycznych analiza przepływu może nie wykryć, że pewna kombinacja ścieżek wykonania i wartości nie jest możliwa. Jako przykład można rozważyć korelację zmiennych:
String flatten(String[] inputs1, String[] inputs2) {
StringBuffer sb1 = null, sb2 = null;
int len = Math.min(inputs1.length, inputs2.length);
for (int i=0; i<len; i++) {
if (sb1 == null) {
sb1 = new StringBuffer();
sb2 = new StringBuffer();
}
sb1.append(inputs1[i]);
sb2.append(inputs2[i]); // ostrzeżenie
}
if (sb1 != null) return sb1.append(sb2).toString();
return "";
}
Kompilator zgłosi potencjalny problem z dostępem do wskaźnika o wartości NULL w wywołaniu sb2.append(..).
Człowiek, który przeczyta ten kod, zauważy że nie ma niebezpieczeństwa, ponieważ zmienne sb1 i sb2
są skorelowane w taki sposób, że obie mają albo nie mają wartości NULL.
We wskazanym wierszu wiemy, że zmienna sb1 nie ma wartości NULL, więc zmienna sb2
również nie ma tej wartości. Bez wnikania w szczegóły przyczyn, dla których taka analiza jest poza możliwościami
kompilatora Java środowiska Eclipse: należy mieć na uwadze, że ta analiza nie ma kompletnych możliwości
wnioskowania i z podejściem pesymistycznym zgłasza niektóre problemy, które bardziej złożona analiza uznałaby
za fałszywe alarmy.
Aby czerpać korzyści z używania analizy przepływu, należy „podpowiadać” kompilatorowi, tak aby
„widział” sposób wnioskowania. Można to zrobić w prosty sposób, na przykład przez rozdzielenie instrukcji if (sb1 == null)
na dwie instrukcje if (po jednej dla każdej zmiennej) - nie jest to wysoka cena za
wskazanie kompilatorowi tego, co dokładnie dzieje się w kodzie.
Dalsza dyskusja na temat tych kwestii znajduje się poniżej.
Ta grupa problemów jest oparta na następującej analogii: w programie korzystającym z typów ogólnych Java 5 dowolne wywołania bibliotek w wersji wcześniejszej niż Java 5 może zwracać typy surowe, tj. zastosowania typów ogólnych, które nie określają argumentów typów konkretnych. Aby dopasować takie wartości w programie z użyciem typów ogólnych, kompilator może dodać niejawną konwersję przy założeniu, że argumenty typu były określone w sposób oczekiwany przez kod po stronie klienta. Kompilator zgłosi ostrzeżenie o użyciu takiej konwersji i będzie kontynuował sprawdzanie typu, zakładając że biblioteka wykonuje operacje poprawnie. Analogicznie typ zwracany bez adnotacji z metody biblioteki może być uważany za typ „surowy” lub „wcześniejszy”. Ponownie w ramach niejawnej konwersji z podejściem optymistycznym jest zakładana oczekiwana specyfikacja. Zgłaszane jest też ostrzeżenie i analityka kontynuuje działanie, zakładając że biblioteka wykonuje operacje poprawnie.
Teoretycznie potrzeba wykonania takiej niejawnej konwersji wskazuje na naruszenie specyfikacji. Jednak w tym przypadku to inny kod może naruszać specyfikację oczekiwaną przez nasz kod. Można też przyjąć (i takie założenie jest przyjmowane), że inny kod wypełnia kontrakt, tylko tego nie deklaruje (ponieważ nie korzysta z adnotacji wartości NULL). W takich sytuacjach może nie być możliwości dokładnego naprawienia problemu z przyczyn organizacyjnych.
@SuppressWarnings("null")
@NonNull Foo foo = Library.getFoo(); // niejawna konwersja
foo.bar();
W powyższym fragmencie kodu jest przyjmowane założenie, że metoda Library.getFoo() zwraca wartość Foo
bez określania adnotacji wartości NULL. Po zintegrowaniu wartości zwracanej z programem z adnotacjami
przez przypisanie do zmiennej lokalnej z adnotacją @NonNull jest zwracane ostrzeżenie
o niesprawdzonej konwersji.
Dodanie odpowiedniej instrukcji SuppressWarnings("null") do tej deklaracji
stanowi potwierdzenie uwzględnienia niejawnego niebezpieczeństwa i zaakceptowania konieczności sprawdzenia, że biblioteka
w rzeczywistości działa zgodnie z oczekiwaniami.
Jeśli analiza przepływu nie wykrywa, że wartość faktycznie nie obejmuje wartości NULL, najprostszą strategią
jest dodanie nowej zmiennej lokalnej w zakresie z adnotacją @NonNull.
Następnie, jeśli wiesz na pewno, że wartość przypisana do tej zmiennej lokalnej nigdy nie będzie miała wartości NULL w czasie wykonania, możesz użyć metody pomocniczej:
static @NonNull <T> T assertNonNull(@Nullable T value, @Nullable String msg) {
if (value == null) throw new AssertionError(msg);
return value;
}
@NonNull MyType foo() {
if (isInitialized()) {
MyType couldBeNull = getObjectOrNull();
@NonNull MyType theValue = assertNonNull(couldBeNull,
"Wartość powinna być różna od NULL, ponieważ aplikacja " +
"jest już w tym momencie w pełni zainicjowana.");
return theValue;
}
return new MyTypeImpl();
}
Użycie powyższej metody assertNonNull() jest zaakceptowaniem odpowiedzialności za
spełnianie tej asercji przez cały czas wykonywania.
Jeśli nie jest to rozwiązanie konkretnego problemu, zmienne lokalne z adnotacjami wciąż mogą ograniczyć liczbę miejsc i przyczyn, dla których
analiza wykrywa potencjalną wartość NULL w określonym położeniu.
W czasie wydawania narzędzi JDT w wersji 3.8.0 zbieranie wskazówek w zakresie adoptowania adnotacji wartości NULL jest wciąż w toku. Z tego powodu te informacje są aktualnie obsługiwane w serwisie Eclipse wiki.