Dlaczego Java nie pozwala na zgłoszenie sprawdzonego wyjątku z bloku statycznej inicjalizacji?

135

Dlaczego Java nie pozwala na zgłoszenie sprawdzonego wyjątku z bloku statycznej inicjalizacji? Jaki był powód tej decyzji projektowej?

missingfaktor
źródło
Jaki wyjątek chciałbyś rzucić w jakiej sytuacji w statycznym bloku?
Kai Huppmann
1
Nie chcę robić czegoś takiego. Chcę tylko wiedzieć, dlaczego wyłapywanie zaznaczonych wyjątków wewnątrz bloku statycznego jest obowiązkowe.
missingfaktor
Jak można by się spodziewać obsługi zaznaczonego wyjątku? Jeśli ci to przeszkadza, po prostu wyrzuć ponownie przechwycony wyjątek, rzucając new RuntimeException ("Telling message", e);
Thorbjørn Ravn Andersen
18
@ ThorbjørnRavnAndersen Java faktycznie zapewnia typ wyjątku dla tej sytuacji: docs.oracle.com/javase/6/docs/api/java/lang/ ...
smp7d
@ smp7d Zobacz odpowiedź kevinarpe poniżej i jej komentarz od StephenC. To naprawdę fajna funkcja, ale ma pułapki!
Benj

Odpowiedzi:

122

Ponieważ nie jest możliwe obsłużenie tych sprawdzonych wyjątków w twoim źródle. Nie masz żadnej kontroli nad procesem inicjalizacji, a statyczne bloki {} nie mogą być wywoływane ze źródła, aby móc je otoczyć try-catch.

Ponieważ nie możesz obsłużyć żadnego błędu wskazywanego przez sprawdzony wyjątek, zdecydowano się zabronić wyrzucania statycznych bloków sprawdzonych wyjątków.

Blok statyczny nie może zgłaszać sprawdzonych wyjątków, ale nadal umożliwia zgłaszanie wyjątków niezaznaczonych / środowiska uruchomieniowego. Ale z powyższych powodów nie byłbyś w stanie sobie z nimi poradzić.

Podsumowując, ograniczenie to uniemożliwia (lub przynajmniej utrudnia) deweloperowi zbudowanie czegoś, co może skutkować błędami, z których aplikacja nie byłaby w stanie odzyskać.

Kosi2801
źródło
69
W rzeczywistości ta odpowiedź jest niedokładna. MOŻESZ rzucać wyjątki w statycznym bloku. To, czego nie można zrobić, to zezwolić zaznaczonemu wyjątkowi na propagację z bloku statycznego.
Stephen C
16
MOŻESZ obsłużyć ten wyjątek, jeśli sam wykonujesz dynamiczne ładowanie klas za pomocą Class.forName (..., true, ...); To prawda, nie jest to coś, z czym często się spotykasz.
LadyCailin
2
static {throw new NullPointerExcpetion ()} - to również się nie skompiluje!
Kirill Bazarov
4
@KirillBazarov Klasa ze statycznym inicjatorem, która zawsze powoduje wyjątek, nie zostanie skompilowana (bo po co?). Zawiń to wyrażenie throw w klauzulę if i gotowe.
Kallja
2
@Ravisha, ponieważ w takim przypadku nie ma szans, aby inicjator w żadnym przypadku zakończył się normalnie. W przypadku try-catch println może nie wyrzucić wyjątku i dlatego inicjator ma szansę zakończyć się bez wyjątku. Jest to bezwarunkowy wynik wyjątku, który sprawia, że ​​jest to błąd kompilacji. Zobacz JLS: docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.7 Ale kompilator nadal można oszukać, dodając prosty warunek w twoim przypadku:static { if(1 < 10) { throw new NullPointerException(); } }
Kosi2801
67

Możesz obejść ten problem, przechwytując każdy zaznaczony wyjątek i zgłaszając go ponownie jako niezaznaczony wyjątek. Ten niekontrolowany klasa wyjątek działa również owijki: java.lang.ExceptionInInitializerError.

Przykładowy kod:

protected static class _YieldCurveConfigHelperSingleton {

    public static YieldCurveConfigHelper _staticInstance;

    static {
        try {
            _staticInstance = new YieldCurveConfigHelper();
        }
        catch (IOException | SAXException | JAXBException e) {
            throw new ExceptionInInitializerError(e);
        }
    }
}
kevinarpe
źródło
1
@DK: Być może Twoja wersja Javy nie obsługuje tego typu klauzuli catch. Spróbuj: catch (Exception e) {zamiast tego.
kevinarpe
4
Tak, możesz to zrobić, ale to naprawdę zły pomysł. Niezaznaczony wyjątek stawia klasę i wszelkie inne klasy, które są od niej zależne, stan niepowodzenia, który można rozwiązać tylko przez wyładowanie klas. Jest to zazwyczaj niemożliwe i System.exit(...)(lub równoważne) jest jedyną opcją,
Stephen C
1
@StephenC Czy możemy pomyśleć, że jeśli klasa „nadrzędna” nie ładuje się, to de facto nie jest konieczne ładowanie jej klas zależnych, ponieważ Twój kod nie będzie działał? Czy mógłbyś podać przykład przypadku, w którym i tak konieczne byłoby załadowanie takiej klasy zależnej? Dzięki
Benj
Co powiesz na… jeśli kod próbuje załadować go dynamicznie; np. przez Class.forName.
Stephen C
21

Musiałoby to wyglądać tak (to nie jest poprawny kod Java)

// Not a valid Java Code
static throws SomeCheckedException {
  throw new SomeCheckedException();
}

ale jak by reklama, gdzie ją złapałeś? Zaznaczone wyjątki wymagają wyłapania. Wyobraź sobie kilka przykładów, które mogą zainicjować klasę (lub nie, ponieważ jest już zainicjowana) i aby zwrócić uwagę na złożoność, którą wprowadziłaby, umieściłem przykłady w innym statycznym inicjatorze:

static {
  try {
     ClassA a = new ClassA();
     Class<ClassB> clazz = Class.forName(ClassB.class);
     String something = ClassC.SOME_STATIC_FIELD;
  } catch (Exception oops) {
     // anybody knows which type might occur?
  }
}

I kolejna paskudna rzecz -

interface MyInterface {
  final static ClassA a = new ClassA();
}

Wyobraź sobie, że klasa A miała statyczny inicjator rzucający sprawdzony wyjątek: w tym przypadku MyInterface (który jest interfejsem z „ukrytym” statycznym inicjatorem) musiałby zgłosić wyjątek lub obsłużyć go - obsługa wyjątków w interfejsie? Lepiej zostaw to tak, jak jest.

Andreas Dolk
źródło
7
mainmoże zgłaszać zaznaczone wyjątki. Oczywiście nie można sobie z nimi poradzić.
Mechaniczny ślimak
@Mechanicalsnail: Ciekawy punkt. W moim mentalnym modelu Javy zakładam, że istnieje „magiczny” (domyślny) Thread.UncaughtExceptionHandler dołączony do działającego wątku, main()który drukuje wyjątek ze śladem stosu do System.err, a następnie wywołuje System.exit(). Ostatecznie odpowiedź na to pytanie brzmi prawdopodobnie: „ponieważ tak powiedzieli projektanci Javy”.
kevinarpe
7

Dlaczego Java nie pozwala na zgłoszenie sprawdzonego wyjątku z bloku statycznej inicjalizacji?

Technicznie możesz to zrobić. Jednak zaznaczony wyjątek musi zostać przechwycony w bloku. Zaznaczony wyjątek nie może być propagowany poza blok.

Z technicznego punktu widzenia możliwe jest również zezwolenie niezaznaczonemu wyjątkowi na propagację z bloku 1 statycznego inicjatora . Ale robienie tego celowo jest naprawdę złym pomysłem! Problem polega na tym, że sama maszyna JVM przechwytuje niezaznaczony wyjątek, opakowuje go i ponownie zgłasza jako plik ExceptionInInitializerError.

Uwaga: to Errornie jest zwykły wyjątek. Nie powinieneś próbować go odzyskać.

W większości przypadków wyjątku nie można złapać:

public class Test {
    static {
        int i = 1;
        if (i == 1) {
            throw new RuntimeException("Bang!");
        }
    }

    public static void main(String[] args) {
        try {
            // stuff
        } catch (Throwable ex) {
            // This won't be executed.
            System.out.println("Caught " + ex);
        }
    }
}

$ java Test
Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.RuntimeException: Bang!
    at Test.<clinit>(Test.java:5)

Nigdzie nie możesz umieścić try ... catchw powyższym miejscu, aby złapać ExceptionInInitializerError2 .

W niektórych przypadkach możesz to złapać. Na przykład, jeśli wyzwoliłeś inicjalizację klasy przez wywołanie Class.forName(...), możesz zawrzeć wywołanie w a tryi złapać albo ExceptionInInitializerErrornastępny NoClassDefFoundError.

Jeśli jednak spróbujesz odzyskać siły , ExceptionInInitializerErrormożesz napotkać blokadę na drogach. Problem polega na tym, że przed zgłoszeniem błędu maszyna JVM oznacza klasę, która spowodowała problem, jako „nie powiodła się”. Po prostu nie będziesz w stanie go używać. Ponadto wszelkie inne klasy, które są zależne od klasy, która zakończyła się niepowodzeniem, również przejdą w stan niepowodzenia, jeśli spróbują zainicjować. Jedynym wyjściem jest zwolnienie wszystkich klas, które zakończyły się niepowodzeniem. Że może być wykonalne dla kodu dynamicznie załadowanego 3 , ale w ogóle nie jest.

1 - jest to błąd kompilacji, jeśli blok statyczny bezwarunkowo zgłasza niesprawdzony wyjątek.

2 - Ty może być w stanie przechwycić go rejestrując domyślnej obsługi przechwycony wyjątek, ale to nie będzie można odzyskać, ponieważ „głównym” nić nie można uruchomić.

3 - Jeśli chcesz odzyskać klasy, które się nie powiodły, musisz pozbyć się modułu ładującego klasy, który je ładował.


Jaki był powód tej decyzji projektowej?

Ma to na celu ochronę programisty przed pisaniem kodu, który rzuca wyjątki, których nie można obsłużyć!

Jak widzieliśmy, wyjątek w inicjatorze statycznym zamienia typową aplikację w cegłę. Najlepszą rzeczą, jaką mogliby zrobić projektanci języka, jest potraktowanie zaznaczonego przypadku jako błędu kompilacji. (Niestety nie jest to praktyczne również w przypadku niezaznaczonych wyjątków).


OK, więc co należy zrobić, jeśli Twój kod „musi” zgłaszać wyjątki w statycznym inicjatorze. Zasadniczo istnieją dwie alternatywy:

  1. Jeśli (pełne!) Odzyskanie z wyjątku w bloku jest możliwe, zrób to.

  2. W przeciwnym razie zrestrukturyzuj swój kod, aby inicjalizacja nie odbywała się w statycznym bloku inicjalizacji (lub w inicjatorach zmiennych statycznych).

Stephen C.
źródło
Czy są jakieś ogólne zalecenia dotyczące struktury kodu tak, aby nie wykonywał żadnej statycznej inicjalizacji?
MasterJoe2,
1
1) Nie mam. 2) Źle brzmią. Zobacz komentarze, które na ich temat zostawiłem. Ale powtarzam tylko to, co powiedziałem w mojej odpowiedzi powyżej. Jeśli przeczytasz i zrozumiesz moją odpowiedź, będziesz wiedział, że te „rozwiązania” nie są rozwiązaniami.
Stephen C,
4

Spójrz na specyfikacje języka Java : stwierdzono, że jest to błąd czasu kompilacji, jeśli inicjator statyczny nie powiedzie się i może zakończyć się nagle z zaznaczonym wyjątkiem.

Laurent Etiemble
źródło
5
To jednak nie odpowiada na pytanie. zapytał, dlaczego jest to błąd czasu kompilacji.
Winston Smith
Hmm, więc wyrzucenie dowolnego RuntimeError powinno być możliwe, ponieważ JLS wspomina tylko o sprawdzonych wyjątkach.
Andreas Dolk
Zgadza się, ale nigdy nie zobaczysz śladu stosu. Dlatego musisz uważać na statyczne bloki inicjalizacyjne.
EJB
2
@EJB: To jest nieprawidłowe. Właśnie go wypróbowałem, a poniższy kod dał mi wizualny ślad stosu: public class Main { static { try{Class.forName("whathappenswhenastaticblockthrowsanexception");} catch (ClassNotFoundException e){throw new RuntimeException(e);} } public static void main(String[] args){} }Wyjście:Exception in thread "main" java.lang.ExceptionInInitializerError Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: whathappenswhenastaticblockthrowsanexception at Main.<clinit>(Main.java:6) Caused by: java.lang.ClassNotFoundException: whathappen...
Konrad Höffner
Część „Spowodowane przez” pokazuje ślad stosu, który prawdopodobnie bardziej Cię interesuje.
LadyCailin,
2

Ponieważ żaden kod, który piszesz, nie może wywołać statycznego bloku inicjalizacji, nie jest użyteczne rzucanie check exceptions. Gdyby to było możliwe, co zrobiłaby jvm, gdy zostaną wyrzucone sprawdzone wyjątki? Runtimeexceptionssą rozmnażane.

fastcodejava
źródło
1
Cóż, tak, teraz rozumiem. To było bardzo głupie z mojej strony, zadając takie pytanie. Ale niestety ... Nie mogę go teraz usunąć. :( Niemniej jednak +1 za odpowiedź ...
missingfaktor
1
@fast, Właściwie sprawdzone wyjątki NIE są konwertowane na RuntimeExceptions. Jeśli sam napiszesz kod bajtowy, możesz rzucić zaznaczone wyjątki wewnątrz statycznego inicjatora do zawartości twojego serca. JVM w ogóle nie przejmuje się sprawdzaniem wyjątków; jest to konstrukcja czysto języka Java.
Antymon
0

Na przykład: Spring's DispatcherServlet (org.springframework.web.servlet.DispatcherServlet) obsługuje scenariusz, który wyłapuje sprawdzony wyjątek i wyrzuca inny niezaznaczony wyjątek.

static {
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized
    // by application developers.
    try {
        ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
        defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
        throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage());
    }
pcdhan
źródło
1
To podejście do problemu polegającego na tym, że nie można przechwycić niesprawdzonego wyjątku. Zamiast tego umieszcza klasę i wszystkie inne klasy, które od niej zależą, w stan nieodwracalny.
Stephen C,
@StephenC - Czy mógłbyś podać prosty przykład, w którym chcielibyśmy mieć stan umożliwiający odzyskanie?
MasterJoe2,
Hipotetycznie ... gdybyś chciał móc odzyskać dane z IOException, aby aplikacja mogła kontynuować. Jeśli chcesz to zrobić, musisz przechwycić wyjątek i faktycznie go obsłużyć ... a nie zgłaszać niezaznaczonego wyjątku.
Stephen C
-5

Jestem w stanie skompilować, zgłaszając również zaznaczony wyjątek ....

static {
    try {
        throw new IOException();
    } catch (Exception e) {
         // Do Something
    }
}
user2775569
źródło
3
Tak, ale łapiesz to w bloku statycznym. Nie możesz wyrzucić sprawdzonego wyjątku z wnętrza bloku statycznego na zewnątrz.
ArtOfWarfare