java: „final” System.out, System.in i System.err?


80

System.outjest zadeklarowany jako public static final PrintStream out.

Ale możesz zadzwonić, System.setOut()aby go ponownie przypisać.

Co? Jak to możliwe, jeśli tak jest final?

(ten sam punkt dotyczy System.ini System.err)

A co ważniejsze, jeśli możesz zmutować publiczne statyczne pola końcowe, co to oznacza, jeśli chodzi o gwarancje (jeśli w ogóle), które finalCi dają? (Nigdy nie zdawałem sobie sprawy, że System.in/out/err zachowuje się jak finalzmienne)


3
Ostateczne pola nie cieszą się wieloma korzyściami przez samą maszynę JVM, chociaż są ściśle sprawdzane przez weryfikatora. Istnieją również sposoby modyfikowania końcowych pól, ale nie za pomocą standardowego kodu java (ponieważ jest on przedmiotem weryfikatora). Odbywa się to za pośrednictwem Unsafe i ujawnia w javie za pośrednictwem Field.set (wymaga access true), który kompiluje się do wspomnianych Unsafe. Również JNI może to zrobić, dlatego JVM nie jest tak chętny do prób optymalizacji ... {może powinienem był
sformułować

Odpowiedzi:


57

JLS 17.5.4 Pola chronione przed zapisem :

Zwykle końcowe pola statyczne nie mogą być modyfikowane. Jednak System.in, System.outi System.errsą ostatecznymi polami statycznymi, które ze starszych powodów muszą mieć możliwość zmiany za pomocą metod System.setIn, System.setOuti System.setErr. Odnosimy się do tych pól jako chronionych przed zapisem, aby odróżnić je od zwykłych pól końcowych.

Kompilator musi traktować te pola inaczej niż inne końcowe pola. Na przykład odczyt zwykłego pola końcowego jest „odporny” na synchronizację: bariera związana z blokadą lub odczytem zmiennym nie musi wpływać na wartość odczytywaną z pola końcowego. Ponieważ wartość pól chronionych przed zapisem może się zmieniać, zdarzenia synchronizacji powinny mieć na nie wpływ. Dlatego semantyka nakazuje, aby te pola były traktowane jak zwykłe pola, których nie można zmienić za pomocą kodu użytkownika, chyba że ten kod użytkownika znajduje się w Systemklasie.

Nawiasem mówiąc, w rzeczywistości możesz mutować finalpola przez odbicie, wywołując setAccessible(true)je (lub używając Unsafemetod). Takie techniki są używane podczas deserializacji, przez Hibernate i inne frameworki itp., Ale mają jedno ograniczenie: kod, który widział wartość końcowego pola przed modyfikacją, nie gwarantuje, że zobaczy nową wartość po modyfikacji. Cechą szczególną tych pól jest to, że są one wolne od tego ograniczenia, ponieważ są one traktowane w specjalny sposób przez kompilator.


4
Niech FSM pobłogosławi stary kod za piękny sposób, w jaki zagraża przyszłemu projektowi!
Sanki

1
>> Ta technika jest używana podczas deserializacji << to teraz nie jest prawdą, desereliazacja używa Niebezpiecznego (szybszego)
bestsss

1
Ważne jest, aby zrozumieć, że setAccessible(true)działa tylko dla staticpól niebędących polami, co sprawia, że ​​nadaje się do zadania pomocy w deserializacji lub klonowaniu kodu, ale nie ma możliwości zmiany static finalpól. Dlatego cytowany tekst zaczyna się od słówZwykle końcowe pola statyczne nie mogą być modyfikowane ”, odnosząc się do final staticcharakteru tych pól i trzech wyjątków. W innym miejscu omówiono przypadek pól instancji.
Holger

Zastanawiam się, dlaczego po prostu nie zdjęli finalmodyfikatora; wydaje się prostsze niż wszystkie te „chronione przed zapisem” rzeczy. Jestem pewien, że to nie jest przełomowa zmiana.
Mark VY

@Holger Istnieje sposób na zmianę statycznych pól końcowych (do Java 8). klasa publiczna OnePrinter {prywatna statyczna końcowa Integer ONE = 1; public static void printOne () {System.out.println (ONE); }} i wtedy możesz Field z = OnePrinter.class.getDeclaredField ("ONE"); z.setAccessible (true); Pole f = Field.class.getDeclaredField ("modyfikatory"); int modifiers = z.getModifiers (); f.setAccessible (true); f.set (z, modyfikatory & ~ Modifier.FINAL); z.set (null, 2); OnePrinter.printOne ();
Peter Verhas

30

Java używa natywnej metody do implementacji setIn(), setOut()a setErr().

Na moim JDK1.6.0_20 setOut()wygląda to tak:

public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}

...

private static native void setOut0(PrintStream out);

Nadal nie możesz "normalnie" ponownie przypisać finalzmiennych, a nawet w tym przypadku nie możesz bezpośrednio zmieniać przypisania pola (tj. Nadal nie możesz skompilować " System.out = myOut"). Metody natywne pozwalają na pewne rzeczy, których po prostu nie można wykonać w zwykłej Javie, co wyjaśnia, dlaczego istnieją ograniczenia dotyczące metod natywnych, takie jak wymóg podpisania apletu w celu korzystania z natywnych bibliotek.


1
OK, więc to tylne drzwi do czystej semantyki Javy ... czy mógłbyś odpowiedzieć na część pytania, które dodałem, a mianowicie, czy możesz zmienić przypisanie strumieni, czy finalma tutaj jakieś znaczenie?
Jason S

1
Prawdopodobnie jest to ostateczne, więc nie można zrobić czegoś takiego jak System.out = new SomeOtherImp (). Ale nadal możesz używać seterów używając podejścia natywnego, jak widać powyżej.
Amir Raminfar

Wydaje mi się, że w tym przypadku wywołania natywnych metod setIn0 i setOut0 naprawdę zmodyfikują wartość końcowej zmiennej, natywne metody prawdopodobnie to potrafią ... To tak, jakby używać kodów w grach: S
Danilo Tommasina

@Danilo, tak, to modyfikuje :)
bestsss

@Jason, brak możliwości bezpośredniego ustawienia wymaga kontroli bezpieczeństwa podczas wywoływania setIn / setErr. wszystko uczciwe i kwadratowe. Przykład, w którym java modyfikuje końcowe pola: java.util.Random (ziarno pola)
bestsss

7

Aby rozszerzyć to, co powiedział Adam, oto implikacja:

public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}

a setOut0 jest zdefiniowane jako:

private static native void setOut0(PrintStream out);

6

Zależy od realizacji. Ostatni może nigdy się nie zmienić, ale może być proxy / adapterem / dekoratorem dla rzeczywistego strumienia wyjściowego, setOut może na przykład ustawić element członkowski, do którego faktycznie zapisuje element wyjściowy. W praktyce jednak jest ustawiony natywnie.


1

outktóra jest zadeklarowana jako final w klasie System jest zmienny poziom klasy. gdzie jak out, który jest w poniższej metodzie, jest zmienną lokalną. nie jesteśmy w miejscu, w którym przekazujemy poziom klasy, który jest w rzeczywistości ostatnim poziomem tej metody

public static void setOut(PrintStream out) {
  checkIO();
  setOut0(out);
    }

użycie powyższej metody jest następujące:

System.setOut(new PrintStream(new FileOutputStream("somefile.txt")));

teraz dane zostaną przekierowane do pliku. Mam nadzieję, że to wyjaśnienie ma sens.

Więc nie ma tu roli natywnych metod lub refleksji w zmianie celu końcowego słowa kluczowego.


1
setOut0 modyfikuje zmienną klasy, która jest ostateczna.
fgb

0

Jeśli chodzi o to, jak, możemy spojrzeć na kod źródłowy, aby java/lang/System.c:

/*
 * The following three functions implement setter methods for
 * java.lang.System.{in, out, err}. They are natively implemented
 * because they violate the semantics of the language (i.e. set final
 * variable).
 */
JNIEXPORT void JNICALL
Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
{
    jfieldID fid =
        (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
    if (fid == 0)
        return;
    (*env)->SetStaticObjectField(env,cla,fid,stream);
}

...

Innymi słowy, JNI potrafi „oszukiwać”. ; )


-2

Myślę, że setout0modyfikuje zmienną na poziomie lokalnym out, nie może modyfikować zmiennej na poziomie klasy out.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.