java: "cuối cùng" System.out, System.in và System.err?


80

System.outđược khai báo là public static final PrintStream out.

Nhưng bạn có thể gọi System.setOut()để chỉ định lại nó.

Huh? Làm thế nào điều này là có thể nếu nó final?

(cùng điểm áp dụng cho System.inSystem.err)

Và quan trọng hơn, nếu bạn có thể thay đổi các trường cuối cùng tĩnh công khai, điều này có nghĩa là gì đối với các đảm bảo (nếu có) finalcung cấp cho bạn? (Tôi chưa bao giờ nhận ra và cũng không mong đợi System.in/out/err hoạt động như finalcác biến)


3
Các trường cuối cùng không được hưởng nhiều lợi ích bởi chính JVM, mặc dù chúng được kiểm tra nghiêm ngặt bởi người xác minh. Có nhiều cách để sửa đổi các trường cuối cùng nhưng không phải thông qua mã java tiêu chuẩn (vì nó là một chủ đề của trình xác minh). Nó được thực hiện thông qua Không an toàn và được hiển thị trong java thông qua Field.set (cần truy cập đúng), biên dịch thành nội dung Không an toàn đã đề cập. Ngoài ra JNI cũng có thể làm được, do đó JVM không quá quan tâm đến việc cố gắng tối ưu hóa ... {có lẽ tôi nên cấu trúc nhận xét như một câu trả lời nhưng meh}
bestsss

Câu trả lời:


57

JLS 17.5.4 Ghi các trường được bảo vệ :

Thông thường, các trường tĩnh cuối cùng có thể không được sửa đổi. Tuy nhiên System.in, System.outSystem.errlà lĩnh vực tĩnh thức rằng, vì lý do di sản, phải được phép thay đổi bằng các phương pháp System.setIn, System.setOutSystem.setErr. Chúng tôi gọi các trường này là được bảo vệ chống ghi để phân biệt chúng với các trường cuối cùng thông thường.

Trình biên dịch cần xử lý các trường này khác với các trường cuối cùng khác. Ví dụ: việc đọc một trường cuối cùng thông thường là "miễn nhiễm" với đồng bộ hóa: rào cản liên quan đến khóa hoặc đọc biến động không phải ảnh hưởng đến giá trị được đọc từ trường cuối cùng. Vì giá trị của các trường được bảo vệ chống ghi có thể thay đổi, các sự kiện đồng bộ hóa sẽ có ảnh hưởng đến chúng. Do đó, ngữ nghĩa ra lệnh rằng các trường này được coi như các trường bình thường mà mã người dùng không thể thay đổi, trừ khi mã người dùng đó nằm trong Systemlớp.

Nhân tiện, thực sự bạn có thể thay đổi finalcác trường thông qua phản chiếu bằng cách gọi setAccessible(true)chúng (hoặc bằng cách sử dụng Unsafecác phương thức). Các kỹ thuật như vậy được sử dụng trong quá trình giải mã hóa, bởi Hibernate và các khung công tác khác, v.v., nhưng chúng có một hạn chế: mã đã nhìn thấy giá trị của trường cuối cùng trước khi sửa đổi không được đảm bảo sẽ thấy giá trị mới sau khi sửa đổi. Điều đặc biệt về các trường được đề cập là chúng không có giới hạn này vì chúng được trình biên dịch xử lý theo cách đặc biệt.


4
Cầu mong FSM phù hộ cho mã kế thừa vì cách đáng yêu mà nó thỏa hiệp với thiết kế trong tương lai!
Xe trượt

1
>> Kỹ thuật này được sử dụng trong quá trình deserialization << điều này không đúng bây giờ, desereliazation sử dụng Không an toàn (nhanh hơn)
bestsss

1
Điều quan trọng là phải hiểu rằng setAccessible(true)chỉ hoạt động cho các statictrường không phải là trường, điều này làm cho nó phù hợp với nhiệm vụ giúp giải mã hóa hoặc sao chép mã, nhưng không có cách nào để thay đổi static finalcác trường. Đó là lý do tại sao văn bản được trích dẫn bắt đầu bằng " Thông thường, các trường tĩnh cuối cùng có thể không được sửa đổi ", đề cập đến final staticbản chất của các trường này và ba trường hợp ngoại lệ. Trường hợp các trường cá thể được thảo luận ở một nơi khác.
Holger

Tôi tự hỏi tại sao họ không chỉ đơn giản là bỏ công cụ finalsửa đổi; có vẻ đơn giản hơn tất cả những thứ "chống ghi" này. Tôi khá chắc rằng đó không phải là một thay đổi đột phá.
Đánh dấu VY

@Holger Có một cách để thay đổi các trường cuối cùng tĩnh (lên đến Java 8). public class OnePrinter {private static final Integer ONE = 1; public static void printOne () {System.out.println (ONE); }} và sau đó bạn có thể Field z = OnePrinter.class.getDeclaredField ("ONE"); z.setAccessible (true); Trường f = Field.class.getDeclaredField ("sửa đổi"); int modifier = z.getModifiers (); f.setAccessible (true); f.set (z, modifier & ~ Modifier.FINAL); z.set (null, 2); OnePrinter.printOne ();
Peter Verhas,

30

Java sử dụng một phương pháp tự nhiên để thực hiện setIn(), setOut()setErr().

Trên JDK1.6.0_20 của tôi, setOut()trông như thế này:

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

...

private static native void setOut0(PrintStream out);

Bạn vẫn không thể gán lại finalcác biến một cách "bình thường" và ngay cả trong trường hợp này, bạn không trực tiếp gán lại trường (tức là bạn vẫn không thể biên dịch " System.out = myOut"). Các phương thức gốc cho phép một số thứ mà bạn không thể thực hiện trong Java thông thường, điều này giải thích tại sao có những hạn chế với các phương thức gốc, chẳng hạn như yêu cầu một applet phải được ký để sử dụng các thư viện gốc.


1
OK, vì vậy đó là một cửa sau xung quanh ngữ nghĩa Java thuần túy ... bạn có thể trả lời phần câu hỏi tôi đã thêm, cụ thể là nếu bạn có thể gán lại các luồng, finalthực sự có bất kỳ ý nghĩa nào ở đây không?
Jason S

1
Nó có thể là cuối cùng để người ta không thể làm điều gì đó như System.out = new SomeOtherImp (). Nhưng bạn vẫn có thể sử dụng bộ định tuyến bằng cách sử dụng cách tiếp cận gốc như bạn thấy ở trên.
Amir Raminfar

Tôi đoán trong trường hợp này các cuộc gọi đến các setIn0 nguồn gốc và phương pháp setOut0 thực sự sẽ thay đổi giá trị của một biến thức, phương pháp tự nhiên có lẽ có thể làm điều đó ... Nó giống như sử dụng mã cheat trong các trò chơi: S
Danilo Tommasina

@Danilo, vâng nó có sửa đổi :)
bestsss

@Jason, không thể đặt trực tiếp nó yêu cầu kiểm tra bảo mật trong khi gọi setIn / setErr. tất cả đều công bằng và vuông vắn. Ví dụ trong đó java sửa đổi các trường cuối cùng: java.util.Random (hạt giống trường)
bestsss

7

Để mở rộng những gì Adam đã nói, đây là sự cấy ghép:

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

và setOut0 được định nghĩa là:

private static native void setOut0(PrintStream out);

6

Phụ thuộc vào việc thực hiện. Cái cuối cùng có thể không bao giờ thay đổi nhưng nó có thể là proxy / adapter / decorator cho luồng đầu ra thực tế, ví dụ setOut có thể thiết lập một thành viên mà thành viên out thực sự ghi vào. Tuy nhiên, trong thực tế, nó được thiết lập nguyên bản.


1

biến outđược khai báo là cuối cùng trong lớp Hệ thống là một biến mức lớp. trong đó as ra trong phương thức dưới đây là một biến cục bộ. chúng ta không ở đâu vượt qua cấp độ lớp thực sự là cấp độ cuối cùng vào phương thức này

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

cách sử dụng phương pháp trên như sau:

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

bây giờ dữ liệu sẽ được chuyển hướng sang tệp. hy vọng lời giải thích này có ý nghĩa.

Vì vậy, không có vai trò của các phương pháp gốc hoặc phản xạ ở đây trong việc thay đổi mục đích của từ khóa cuối cùng.


1
setOut0 đang sửa đổi biến lớp, là biến cuối cùng.
fgb

0

Về cách làm, chúng ta có thể xem qua mã nguồn để 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);
}

...

Nói cách khác, JNI có thể "gian lận". ; )


-2

Tôi nghĩ rằng setout0đang sửa đổi biến cấp cục bộ out, nó không thể sửa đổi biến cấp độ lớp out.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.