Статьи

Указатели на Java

Есть ли в Java указатели? Краткий ответ «нет, их нет», и это кажется очевидным для многих разработчиков. Но почему это не так очевидно для других?

Это потому, что ссылки, которые Java использует для доступа к объектам, очень похожи на указатели. Если у вас есть опыт программирования на C до Java, вам может быть проще думать о значениях, которые хранятся в переменных, как указатели, указывающие на некоторые области памяти, содержащие объекты. И это более или менее нормально. Больше, чем больше, но это то, на что мы сейчас посмотрим.

Разница между ссылкой и указателем

Как резюмировал Брайан Агнью о стековом потоке, есть два основных различия.

  1. Там нет указателя арифметики
  2. Ссылки не «указывают» на область памяти

Отсутствует указатель арифметики

Когда у вас есть массив struct в C, память, выделенная для массива, содержит содержимое структур одну за другой. Если у вас есть что-то вроде

1
2
3
4
5
struct circle {
   double radius;
   double x,y;
}
struct circle circles[6];

он будет занимать 6*3*sizeof(double) байтов в памяти (обычно это 144 байта в 64-битной архитектуре) в непрерывной области. Если у вас есть что-то похожее в Java, вам нужен класс ( пока мы не доберемся до Java 10 или более поздней версии ):

1
2
3
4
class Circle {
   double radius;
   double x,y;
}

и массив

1
Circle circles[6];

потребуется 6 ссылок (48 байтов или около того), а также 6 объектов (если некоторые из них не равны нулю), каждые 24 байта данных (или около того) и заголовок объекта (16 байтов). На 64-битной архитектуре это составляет 288 байт, а область памяти не является непрерывной.

Когда вы получаете доступ к элементу, скажем circles[n] массива языка C, код использует арифметику указателей. Он использует адрес, сохраненный в circles указателя, добавляет n раз sizeof(struct circle) (байты), и именно там находятся данные.

Подход Java немного отличается. Он смотрит на circles объекта, являющиеся массивом, вычисляет n элемент (это похоже на C) и извлекает справочные данные, хранящиеся там. После того, как эталонные данные находятся под рукой, он использует их для доступа к объекту из некоторого другого места в памяти, куда ведут эталонные данные.

Обратите внимание, что в этом случае накладные расходы памяти Java составляют 100%, а также число чтений памяти равно 2 вместо 1 для доступа к фактическим данным.

Ссылки не указывают на память

Ссылки на Java не являются указателями. Они содержат какие-то данные указателя или что-то еще, потому что это происходит из природы современной компьютерной архитектуры, но это полностью зависит от реализации JVM, что она хранит в ссылочном значении и как она обращается к объекту, к которому она относится. Это может быть абсолютно нормально, хотя и не слишком эффективной реализацией, чтобы иметь огромный массив указателей, каждый из которых указывает на объект JVM, а ссылки являются индексами этого массива.

В действительности JVM реализует ссылки как некое сочетание указателей, где некоторые биты являются флагами, а некоторые биты «указывают» на некоторую область памяти относительно некоторой области.

Почему JVM делают это вместо указателей?

Причина в сборке мусора. Чтобы реализовать эффективную сборку мусора и избежать фрагментации памяти, JVM регулярно перемещает объекты в памяти. Когда память, занятая объектами, на которые больше нет ссылок, освобождается, и у нас оказывается маленький объект, все еще используемый и на который ссылаются в середине огромного доступного блока памяти, мы не хотим, чтобы этот блок памяти был разделен. Вместо этого JVM перемещает объект в другую область памяти и обновляет все ссылки на этот объект, чтобы отслеживать новое местоположение. Некоторые реализации GC останавливают другие потоки Java на время этих обновлений, так что ни один код Java не использует ссылку, которая не обновлена, но объекты перемещены. Другие реализации GC интегрируются с управлением виртуальной памятью базовой ОС, чтобы вызвать сбой страницы, когда такой доступ происходит, чтобы избежать остановки потоков приложения.

Однако дело в том, что ссылки НЕ являются указателями, и реализация JVM отвечает за то, как она управляет всеми этими ситуациями.

Следующая тема, тесно связанная с этой областью, — передача параметров.

Параметры передаются по значению или по ссылке в Java?

Первым языком программирования, который я изучал в университете, был PASCAL, изобретенный Никлаусом Виртом . На этом языке аргументы процедуры и функции могут передаваться по значению или по ссылке. Когда параметр был передан по ссылке, объявлению аргумента в заголовке процедуры или функции предшествовало ключевое слово VAR . В месте использования функции программисту не разрешается писать выражение в качестве фактического аргумента. Вы должны использовать переменную, и любое изменение аргумента в функции (процедуре) будет влиять на переменную, переданную в качестве аргумента.

Когда вы программируете на языке C, вы всегда передаете значение. Но на самом деле это ложь, потому что вы можете передать значение указателя, который указывает на переменную, которую функция может изменить. То есть, когда вы пишете такие вещи, как char *s в качестве аргумента, а затем функция может изменить символ, на который указывает s или целую строку, если она использует арифметику указателей.

В PASCAL объявление передачи по значению ИЛИ передачи по ссылке находится в объявлении функции (или процедуры). В C вы явно должны написать выражение, подобное &s чтобы передать указатель на переменную s чтобы вызывающий мог изменить его. Конечно, функция также должна быть объявлена ​​для работы с указателем на любой тип s .

Когда вы читаете код PASCAL, вы не можете сказать на месте фактического вызова функции, передан ли аргумент по значению и, следовательно, может быть изменен функцией. В случае C вы должны кодировать его в обоих местах, и всякий раз, когда вы видите, что значение аргумента &s передано, вы можете быть уверены, что функция способна изменить значение s .

Что тогда с Java? Вы можете программировать на Java годами, но можете не сталкиваться с проблемой или думать о ней. Java решает проблему автоматически? Или просто дает решение, которое настолько просто, что двойного подхода по значению / ссылке не существует?

Печальная правда в том, что Java на самом деле скрывает проблему, а не решает ее. До тех пор, пока мы работаем только с объектами, Java передается по ссылке. Независимо от выражения, которое вы пишете в фактический вызов функции, когда результатом является объект, в метод передается ссылка на объект. Если выражение является переменной, то передается ссылка, содержащаяся в переменной (которая является значением переменной, так что это своего рода передача по значению).

Когда вы передаете примитив ( int , boolean т. Д.), Тогда аргумент передается по значению. Если вычисленное выражение приводит к примитиву, то оно передается по значению. Если выражение является переменной, то передается примитивное значение, содержащееся в переменной. Таким образом, мы можем сказать, глядя на три примера языков, которые

  • PASCAL объявляет, как передавать аргументы
  • C вычисляет фактическое значение, где оно передается
  • Java решает в зависимости от типа аргумента

Ява, на мой взгляд, немного грязная. Но я не осознавал этого, потому что этот беспорядок ограничен и хорошо скрыт тем фактом, что коробочные версии примитивов неизменны. Зачем вас беспокоить механизм передачи аргументов, если значение все равно не может быть изменено. Если оно передается по значению: все в порядке. Если он был передан по ссылке, он все еще в порядке, потому что объект неизменен.

Это вызвало бы проблему, если бы упакованные примитивные значения были изменяемы? Мы увидим, когда и когда у нас будут типы значений в Java.

Ссылка: Указатели на Java от нашего партнера JCG Питера Верхаса из блога Java Deep .