Метод print() класса PrintStream

145
22 июля 2019, 07:40

Дорогие камрады! :)

Если честно, то уже давно хотел разобраться в тонкостях работы одного из самых распространнёных методов в языке программирования Java. Да-да, Вы всё правильно поняли, речь идёт о методе System.out.print(); и его вариациах (println(); и printf();). Насколько мне известно, то сам метод описан в классе PrintStream, а переменная out является всего лишь членом класса System, имеющая тип данных PrintStream. Поле out является статическим и проинициализировано значением null при помощи соответствующего null-литерала. Изначально у меня возникли определённые вопросы, которые напрашиваются сами собой. Как же мы можем вызывать какой бы то ни было метод, к примеру, то же метод print(), если вместо адреса объекта в памяти наша переменная ссылочного типа данных хранит значение null (иначе говоря, вообще ничего не хранит)? Но с этим я как-то быстро разобрался, когда увидел, что в порядке расположения статических полей и блоков в исходном коде программы, изначально выполняется статический блок, который содержит следующую инструкцию:

static {
    registerNatives();
} 

Вызываемый метод является не только статическим, но и нативным. Полагаю, что его реализация хранится где-то в самой JVМ, но точно не знаю где. Именно поэтому у меня не было возможности с ней ознакомиться. Также, в документации к исходному коду класса System, я вычитал, что переменная out представляет собой абстракцию «стандартного» выходного потока, который, между тем, уже открыт и готов принять входные данные.

Итак, мой вопрос заключается в следующем. Как же просходит инициализация поля out на самом деле? Правильно ли я понимаю, что изначально выполнение метода registerNatives(); влечёт за собой выполнение других нативных методов, которые также содержаться в классе System, одним из которых, кроме всего прочего, является метод setOut0(PrintStream out);? Где я могу посмотреть реализацию данного метода и какие конкретно действия он выполняет? Верны ли мои предположения о том, что именно данный метод выполняет инициализацию поля out? Буду всем крайне благодарен за Ваши ответы!

P.S. Есть ещё один момент, который мне также не совсем понятен. Если сначала выполняется статический метод registerNatives();, который повлечёт за собой выполнение всех остальных статических нативных методов, то каким образом мы сможем проинициализовать наши поля, при помощи данных методов, если к этому моменту они ещё даже не будут объявлены? В моём понимании, мы дойдём до их объявления исключительно после полного выполнения кода из метода registerNatives();. Если это возможно, то поясните, пожалуйста, данный момент.

Answer 1

Потоки in, out и err инициализируются в методе initPhase1 (initializeSystemClass до JDK 8 включительно), который вызывается уже после registerNatives():

FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
setIn0(new BufferedInputStream(fdIn));
setOut0(new PrintStream(new BufferedOutputStream(fdOut, 128), true));
setErr0(new PrintStream(new BufferedOutputStream(fdErr, 128), true));

Стандартный поток вывода есть ничто иное, как обычный файл, который открывается на дополнение информации. Доступ к ним осуществляет по файловому дескриптору (id в ОС). Они открываются по разному в различных системах, для этого используются нативные методы.

В Unix'о-подобных системах: 0 - является файловым дескриптором стандартного потока ввода, 1 - вывода, 2 - ошибки.

В Java файловый дескриптор инициализируется так:

public static final FileDescriptor out = new FileDescriptor(1);
private FileDescriptor(int fd) {
    this.fd = fd;
    this.append = getAppend(fd);
}
private static native boolean getAppend(int fd);

Таким образом весь вывод в стандартный поток out, сводится к записи в файл. Логичным остаётся вопрос что-же тогда делает метод setOut0? Его реализацию можно найти в файле System.c:

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);
}

Здесь появляется интерфейс JNI - Java Native Interface, при помощи которого можно связать код написанный на C или С++.

Как и ожидается, setOut0 присваивает поток статической переменной. Казалось бы, можно было бы присвоить сразу, но дело в том, что при инициализации статических полей класса System, сама JVM ещё не до конца проинициализирована и нет возможности правильно присвоить что либо потоку out, поэтому ставят null. Однако out инициализируется в initPhase1, который вызывается после того, как JVM будет проинициализирована, но из-за того что out является final, а инициализация статических переменных уже прошла, приходится воспользоваться нативным методом, который присвоит данной переменной поток вывода.

По поводу registerNatives. Дело в том, что нативный код обычно реализуется на C или С++, под определённую ОС.

Метод registerNatives(); не выполняет инициализацию нативных методов, а только создаёт связку имён. Так как обычный механизм JNI требует названий вида Java_Полное_имя_вашего_класса_название_метода. Таким образом для метода java.lang.System.registerNatives имя будет Java_java_lang_System_registerNatives, поэтому в данном нативном месте происходит упрощение имён для дальнейшей разработки на C/C++.

Вот пример кода JNI, функции registerNatives для System, которая регистрирует функцию JVM_SettingOutStream в С.

static JNINativeMethod methods[] = {
    {"setOut0",    "(Ljava/lang/Object;)V",   (void *)&JVM_SettingOutStream},
};
JNIEXPORT void JNICALL
Java_java_lang_System_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
                            methods, sizeof(methods)/sizeof(methods[0]));
}
READ ALSO
Объект события в java

Объект события в java

Для чего нужен объект события?(аргумент реализуемого метода интерфейса)

150
Подключение Google Maps Java и Directions Api

Подключение Google Maps Java и Directions Api

Доброй ночиИмеется проект на Spring Boot

151
Не запускается консольное приложение Windows из приложения Java

Не запускается консольное приложение Windows из приложения Java

Есть кроссплатформенная консольная утилита ring (ставится для работы с программными лицензиями 1С: Предприятия)

182
Раскрывающийся список select и несколько баз данных

Раскрывающийся список select и несколько баз данных

Допустим есть объект organisations, содержащий в каждой ячейке объект, включающий в себя наименование организации, ИНН, ОГРН и адрес, примерно так:

173