Разработка программ. Мои заметки.

January 7, 2017 at 19:04

NullPointerException в Java. Истоки.

1. Откуда ноги растут. Значение, указатель, ссылка.

Вначале немножко букваря для тех, у кого Java есть первый язык программирования и кто на многие вещи пока не обращал пристального человеческого внимания. Люди, пришедшие из “древних” языков, требующих ручного управления памятью (скажем, С/С++), эти детали, конечно же, отлично понимают. Ну… должны во всяком случае.

Современные компьютеры устроены таким образом, что программа, перед выполнением, загружается в оперативную память компьютера (RAM). Там же, в ячейках памяти, хранятся и значения переменных программы во время её выполнения. Каждая такая ячейка имеет свой уникальный адрес, по сути — это номер ячейки. Зная этот номер, мы всегда можем найти нужную ячейку и прочитать хранящееся в ней значение.

Часто возникает необходимость передать какое-то значение в другое место программы. К примеру, в функцию или метод. Если речь идёт об одиночном числе, то его можно запросто скопировать из тех ячеек памяти где оно сейчас хранится и записать в другую группу ячеек, которая является частью кода функции или в какое-то другое место, где функция может это значение прочитать и использовать в своих вычислениях. Другими словами, само число берётся и копируется в другое место памяти компьютера. Это называется передача по значению.

Но есть и другой способ передать куда-то наше число. Мы можем запомнить адрес ячейки памяти, в которой число хранится. И затем, в нужное место передать этот адрес. Адрес — также число, но программа знает, что это число есть пока лишь адрес памяти. Получив этот адрес, функция или метод, которым требуется наше число, могут сами до него добраться. Для этого им нужно по переданному адресу найти ячейку памяти и считать её содержимое. Это и будет требуемое число. Итак, если некая переменная хранит не само число, а адрес ячейки, где это число записано, то такая переменная называется указателем или ссылкой.

Как переменная-указатель, так и переменная-ссылка хранят адрес того места в памяти, где хранится нужное программе число. Когда мы передаём какому-то методу такой вот адрес, по которому “проживает” нужное ему число, то это называется передача параметра по ссылке. Это довольно удобный способ, поскольку далеко не всегда передаётся одинокое сиротливое число, очень часто нужно передать большой кусок информации. И накладно его всякий раз весь копировать из одной группы ячеек памяти в другую (передавать по значению), где он нам понадобился. Гораздо эффективнее передать адрес, начиная с которого расположен этот нужный нам блок. Скажем, стартовый адрес какой-то структуры данных или объекта.

Для иллюстрации можно привести бытовую аналогию с гардеробом. Скажем, попросил вас приятель повесить его пальто в гардероб. А затем, что логично, просит его оттуда забрать. Вы можете сами сходить в гардероб, получить его пальто и вручить товарищу в руки. Это будет передача параметра по значению. Но если вам лень, вы можете отдать приятелю номерок от гардероба и предложить ему самому шевельнуть булками и пойти забрать своё пальто в гардеробе. Это будет передача параметра по ссылке.

Хотя и переменная типа указатель и переменная типа ссылка хранят адрес нужных ячеек памяти, тем не менее работа с переменными таких типов синтаксически по-разному организована в различных языках программирования. И тут начинаются тонкости. К примеру, язык С++ имеет два явно различных типа данных, которые хранят именно адреса других участков памяти: указатель и ссылка. Я коротко поясню это на примере именно этого языка, поскольку С++ язык мощный и популярный. И реализации указателей и ссылок в других языках часто являются комбинацией возможностей, предоставляемых языком С++.

В С++ переменная типа указатель позволяет свободно изменять её содержимое. Другими словами, мы можем присвоить ей любое значение во время выполнения нашей программы. И это значение компилятор всегда трактует именно как адрес ячейки памяти (ну просто потому, что эта переменная названа указателем). Это то, что даёт языкам С и С++ волшебную гибкость в обращении с памятью компьютера. Вы можете залезть в любое место памяти своей программы и что-то там сотворить. Вне адресного пространства программы вас немного ограничит операционная система, но и там есть свои возможности.

Обратная сторона такой свободы — это высокая плата за ошибку. Да как, собственно, и при любой свободе. В случае ошибки и неверно занесённого адреса, указатель может указать на то место в памяти, где хранится совсем не то, что вы там ожидаете нащупать. Со всеми вытекающими оттуда последствиями. Как правило, это ведёт к аварийному завершению программы. Особо “опытные” программисты способны легко “завершить работу” операционной системы в целом. Как вы уже поняли, всё это несколько опасно и требует тщательной отладки. Порой, довольно нетривиальной.

А вот ссылка в С++ намного менее опасна. Значение переменной такого типа изменять нельзя. Вы должны сразу (во время объявления переменной) и навсегда “прицепиться” к какому-то адресу и считывать только то, что лежит там. При этом ссылка всегда “разадресовывается”. Другими словами, при обращении к переменной типа ссылка, вы сразу получаете то значение, которое лежит по адресу, на который эта переменная-ссылка указывает. Вы не должны помнить и учитывать, что в переменной типа ссылка фактически хранится адрес и нужно вот по нему куда-то там сходить и что-то там взять.

Данные, записанные по адресу на который указывает переменная типа ссылка, вам сразу поставляются компилятором при обращении к такой переменной. Компилятор всё делает за вас. Ссылка, по сути, является просто другим именем (синонимом) той переменной, на адрес которой она ссылается. Единожды привязавшись к этой переменной, ссылка больше не может меняться. И именно поэтому относительно безопасна. Она не может хранить недопустимый адрес в памяти. Разумеется, можно придумать трюк, позволяющий это сделать, но это занятие для тех, кто любит экспериментировать с попаданием пули в весомую часть своего тела. Это всё вроде как специфика языков С/С++, но сама терминология обращения с памятью во многом формировалась именно при использовании этих языков. Хотя придумана была немного раньше.

В других языках, переменные, хранящие адрес памяти, часто ведут себя немного иначе. В чём-то удобнее, а в чём-то и опаснее. Возможности и ограничения работы с такими переменными это действительно часть свойств используемого алгоритмического языка. Есть довольно интересный сайт — rosettacode. На этом сайте разные люди описывают реализации одной и той же программной возможности в различных языках программирования. Имеется там и раздел, посвящённый ссылкам и указателям - pointers and references. В нём описано как обращаются с переменными такого типа в нескольких десятках языков, оставивших/оставляющих заметный вклад в человеческой цивилизации.

Теперь мы, вооружённые этими серьёзными низкоуровневыми познаниями, вернёмся к нашему языку Java, чтобы объективно оценить подстерегающие нас там опасности.

Автор — Владимир Рыбов