Дорогие камрады! :)
Если честно, то уже давно хотел разобраться в тонкостях работы одного из самых распространнёных методов в языке программирования 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();
. Если это возможно, то поясните, пожалуйста, данный момент.
Потоки 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]));
}
Виртуальный выделенный сервер (VDS) становится отличным выбором
Есть кроссплатформенная консольная утилита ring (ставится для работы с программными лицензиями 1С: Предприятия)
Допустим есть объект organisations, содержащий в каждой ячейке объект, включающий в себя наименование организации, ИНН, ОГРН и адрес, примерно так: