Где хранятся данные
Полезно отчетливо представлять, что происходит во время работы программы — и в частности, как данные размещаются в памяти. Существует пять разных мест для хранения данных:
1. Регистры. Это самое быстрое хранилище, потому что данные хранятся прямо внутри процессора. Однако количество регистров жестко ограничено, поэтому регистры используются компилятором по мере необходимости. У вас нет прямого доступа к регистрам, вы не сможете найти и малейших следов их поддержки в языке. (С другой стороны, языки С и С++ позволяют порекомендовать компилятору хранить данные в регистрах.)
2. Стек. Эта область хранения данных находится в общей оперативной памяти (RAM), но процессор предоставляет прямой доступ к ней с использованием указателя стека. Указатель стека перемещается вниз для выделения памяти или вверх для ее освобождения. Это чрезвычайно быстрый и эффективный способ размещения данных, по скорости уступающий только регистрам. Во время обработки программы компилятор Java должен знать жизненный цикл данных, размещаемых в стеке. Это ограничение уменьшает гибкость ваших программ, поэтому, хотя некоторые данные Java хранятся в стеке (особенно ссылки на объекты), сами объекты Java не помещаются в стек.
3. Куча. Пул памяти общего назначения (находится также в RAM), в котором размещаются все объекты Java. Преимущество кучи состоит в том, что компилятору не обязательно знать, как долго просуществуют находящиеся там объекты. Таким образом, работа с кучей дает значительное преимущество в гибкости. Когда вам нужно создать объект, вы пишете код с использованием ключевого слова new, и память выделяется из кучи во время выполнения программы. Конечно, за гибкость приходится расплачиваться: выделение памяти из кучи занимает больше времени, чем в стеке (даже если бы вы могли явно создавать объекты в стеке, как в С++).
4. Постоянная память. Значения констант часто встраиваются прямо в код программы, так как они неизменны. Иногда такие данные могут размещаться в постоянной памяти (ROM), если речь идет о «встроенных» системах.
5. Не-оперативная память. Если данные располагаются вне программы, они могут существовать и тогда, когда она не выполняется. Два основных примера: потоковые объекты (streamed objects), в которых объекты представлены в виде потока байтов, обычно используются для посылки на другие машины, и долгоживущие (persistent) объекты, которые запоминаются на диске и сохраняют свое состояние даже после окончания работы программы. Особенностью этих видов хранения данных является возможность перевода объектов в нечто, что может быть сохранено на другом носителе информации, а потом восстановлено в виде обычного объекта, хранящегося в оперативной памяти.
Примитивные типы
Создание объекта в куче для маленькой простой переменной — недостаточно эффективно, а ключевое слово new создает объекты именно в куче. В таких случаях вместо создания переменной с помощью new создается «автоматическая» переменная, не являющаяся ссылкой. Переменная напрямую хранит значение и располагается в стеке, так что операции с ней гораздо производительнее. Такие переменные относят к так называемым «примитивным» типам данных.
В Java размеры всех примитивных типов жестко фиксированы. Они не меняются с переходом на иную машинную архитектуру, как это происходит во многих других языках. Незыблемость размера — одна из причин улучшенной переносимости Java-nporpaмм.
Примитивный тип | Размер, бит | Минимум | Максимум | Тип упаковки |
boolean (логические значения) | — | — | — | Boolean |
char (символьные значения) | Unicode 0 | Unicode 216-1 | Character | |
byte (байт) | -128 | +127 | Byte | |
short (короткое целое) | -215 | +215-1 | Short | |
int (целое) | -231 | +231-1 | Integer | |
long (длинное целое) | -263 | +263-1 | Long | |
float (число с плавающей запятой) | 1.40239846e-45f* | 3.40282347e+38f | Float | |
double (число с повышенной точностью) | 4.94065645841246544e-32** | 1.79769313486231570e+308 | Double | |
Void («пустое» значение) | — | — | — | Void |
Размер типа boolean явно не определяется; указывается лишь то, что этот тип может принимать значения true и false.
Переменные
Переменная — это именованная область памяти, в которой хранятся данные определённого типа.У переменной есть имя и значение. Имя служит для обращения к области памяти, в которой хранится значение. Во время выполнения программы значение переменной можно изменять. Перед использованием любая переменная должна быть объявлена и инициализирована.
Объявление переменной — сообщение компилятору (интерпретатору) имени и типа переменной.
int a;
float b;
Инициализация — присваивание переменной начального значения.
а = 7;
b = 8.1;
Инициализация может также происходить непосредственно при объявлении:
int a = 10;
float b = 12.4;
Классы
Класс является абстрактным типом данных, определяемым пользователем, и представляет собой модель реального объекта в виде данных и функций для работы с ними (Павловская).
Данные класса называются полями, а функции класса — методами. Поля и методы называются членами класса.
Метод в общем виде задается так:
() { }
Описание (определение) класса в простейшем виде выглядит так:
class { }
Например, запишем определение класса Man, но для начала отметим, что в фигурные скобки в Java применяются для объединения нескольких действий в единый блок, что используется для определения методов, классов и т.д. Обратите внимание на то, что в Java точка с запятой не ставится после закрывающей скобки блока.
class Man { // это определение класса
String hairColor = brown; //это задание поля
String getHairColor() { //это определение метода
return hairColor;
}
}
Конкретные переменные типа «класс» называются экземплярами класса, или объектами(Павловская).
Допустим, что у нас есть класс А
class A {
int x = 0;
void f(){}
} [1]
Тогда его экземпляр (объект класса), можно создать так:
A obj = new A();
В этом случае доступ к полям и методам класса можно осуществить так:
obj.x; // доступ к полю класса
obj.f(); /* доступ к методу класса */[2]
Каждый объект обладает состоянием (поля), поведением (методы) и индивидуальностью (каждый объект можно отличить от другого объекта, т.е. каждый объект обладает уникальным адресом в памяти[3]).
Каждый объект умеет выполнять только определённый круг запросов. Запросы, которые вы можете посылать объекту, определяются его интерфейсом, причём интерфейс объекта определяется его типом. Простейшим примером может стать электрическая лампочка:
Light It = new Light();
It.on();
Интерфейс определяет, какие запросы вы вправе делать к определённому объекту. Однако где-то должен существовать и код, выполняющий запросы. Этот код, наряду со скрытыми данными, составляет реализацию.
Операторы
Оператор | Обозначение | Пример | |
Выражение | Результат | ||
Арифметические | |||
присваивания | = | y = 5 | |
сложения | + | y = 2 + 3 | |
вычитания | – | y = 7 – 2 | |
умножения | * | y = 2 * 2 | |
деления на целое | % | y = 9 % 2 | |
деления | / | y = 9 / 2 y = 9. / 2 | 4.5 |
сложение с присваиванием | += | y = 5; y += 2 | |
вычитание с присваиванием | -= | y = 5; y – = 2 | |
умножение с присваиванием | *= | y = 5; y *= 2 | |
деление с присваиванием | /= | y = 5; y /= 2 | |
деление на целое с присваиванием | %= | y = 5; y %= 2 | |
унарное сложение | ++ | x = 7; y = x++ x = 7; y = ++x | y=7, x=8 y=8, x=8 |
унарное вычитание | — | x = 9; y = x— x = 9; y = —x | y=9, x=8 y=8, x=8 |
Логические | |||
не (лог. отрицание) | ! | y = true; !у | false |
и | 5 0 | false | |
или | || | 5 || 0 | true |
Сравнения | |||
равно | = = | 5 = = 5 | true |
меньше | 5 4 | false | |
больше | 5 4 | true | |
меньше или равно | 5 | false | |
больше или равно | = | 5 = 4 | true |
не равно | != | 5 != 4 | true |
Здесь стоит обратить внимания на операторы, выполняющие одновременно действие и присваивание. Разберем к примеру выражение
y += 2
Оно означает, что к значению переменной у прибавляется 2 и полученное новое значение записывается в переменную у. Аналогично действуют и другие подобные выражения.
Унарные операции действуют таким же образом, только увеличивается и уменьшается значения переменной не на произвольную величину, а на единицу, при этом как мы видим у таких операций только один операнд. Отсюда и название унарные.
Если унарные операции идут в сочетании с присваиванием или передаются параметром в функцию, то очень важно, с какой стороны стоит знак операции, справа или слева от операнда. В случае, если знак справа от операнда, сначала происходит присваивание (или передача значения параметром), а затем уже значение самой переменной увеличивается (инкрементируется) или уменьшается (декрементируется). В противном случае (когда знак стоит до операнда), сначала происходит изменение переменной, а затем уже происходит присваивание (или передача значения параметром).
Отметим, что в результате операции деления, примененной к целочисленному типу (т. е. когда и делимое, и делитель — целочисленного типа), дробная часть отбрасывается (заметьте, не округляется до целого, а отбрасывается). В случае же с дробными числами деление работает как мы и привыкли, т. е. дробная часть остается.
Операция деление на целое дает в результате целочисленный остаток от деления целого числа на целое.
Все операции сравнения возвращают результат типа boolean.
Проверка объектов на равенство
Операции отношений == и != также работают с любыми объектами, но их смысл нередко сбивает с толку начинающих программистов на Java. Пример:
public class Equivalence {
public static void main(String[] args) {
Integer nl = new Integer(47);
Integer n2 = new Integer(47);
int n3 = 47;
int n4 = 47;
System.out.println(nl == n2);
System out println(n3 == n4);
}
} /* Output.
false
true
Результаты просто ошеломляют. Почему в первом случае мы получаем ЛОЖЬ при сравнении?
Тут дело вот в чем. Если в первом случае мы имеем дело с объектами из кучи, то n1 и n2 есть ни что иное, как ссылки на эти объекты в куче. Получается в данном случае оператор == сравнивает между собою ссылки, а не значения!!! Естественно, ссылки на эти объекты разные, поэтому и результат false.
Во втором случае мы имеем дело с примитивными типами, которые, как мы знаем, хранятся не в куче, а в системном стеке, и обращение к которым происходит не по ссылке, а напрямую к их значению. Естественно, и результат мы получаем ожидаемый – true.
Как тогда сравнивать между собой объекты из кучи? Для этого все объекты Java имеют метод equals(). По умолчанию метод equals() сравниваем между собой тоже ссылки, но его можно переопределить. Кстати, у подавляющего большинства классов стандартной библиотеки Java этот метод уже переопределен так, чтобы сравнивать именно значения, так что в большинстве случаев переопределять его самостоятельно не понадобится (обычно это требуется для самостоятельно созданных классов).
Итак, если в предыдущем примере мы заменим строку
System.out.println(nl == n2);
на
System.out.println(n1.equals(n2));
то теперь в результат получим true, как мы и ожидали изначально.
Инициализация
Конструктор — метод, предназначенный для инициализации объекта, который вызывается автоматически при создании объекта.
Конструктор всегда:
— имеет то же имя, что и класс;
— не возвращает никакого значения, даже типа void;
— конструктор, вызываемый без параметров, называется конструктором по умолчанию;
— если программист не указал ни одного конструктора, компилятор создает его автоматически;
— конструкторы не наследуются.
class A { // Это и есть конструктор
A { System.out.print(Выполняется конструктор ); }
}
При создании объекта
new A();
выполняется инициализация, т.е. выделяется память и вызывается конструктор, поэтому в выводе мы увидим:
Выполняется конструктор
Подобно любому методу, у конструктора могут быть аргументы, для того чтобы позволить вам указать, как создать объект.
class A {
A(int i){System.out.print(Выполняется конструктор + i);}
}
Теперь после создания объекта
new A(1);
сообщение будет выглядеть так:
Выполняется конструктор 1
Конструкторов может быть несколько, но в этом случае они должны отличаться либо количеством, либо типом принимаемых аргументов, либо и тем и другим.
Если конструктор с аргументами будет единственным конструктором класса, то будет выполняться именно он (просто компилятор не позволит создавать объекты этого класса каким-либо другим способом ).
Если единственным конструктором класса будет конструктор без аргументов и без каких либо действий внутри его тела , тогда такой конструктор можно не определять. Компилятор создаст пустой конструктор автоматически в процессе создания объекта.
Подробнее о классах
static — ключевое слово, указывающее, что член класса является статическим. Т.е. данные или метод не привязаны к определённому экземпляру этого класса. Поэтому, даже если вы никогда не создавали объектов класса, вы можете вызвать статический метод или получить доступ к статическим данным .
Причины использования static:
1) когда некоторые данные должны храниться «в единственном числе» независимо от того, сколько было создано объектов класса.
2) когда вам потребуется метод, не привязанный ни к какому конкретному объекту класса (то есть метод, который можно вызвать даже при полном отсутствии объектов класса) .
final — ключевое слово, означающее «Это нельзя изменить»;
Причины использования final:
1) когда нужна константа времени компиляции, которая никогда не меняется (используется только для примитивных типов);
2) когда надо задать значение, инициализируемое во время работы программы, которое нельзя изменять.
Поле, одновременно объявленное с ключевыми словами static и final, существует в памяти в единственном экземпляре и не может быть изменено.
Для примитивов final делает постоянным значение, но для ссылки на объект постоянной становится ссылка. После того как такая ссылка будет связана с объектом, она уже не сможет указывать на другой объект. Впрочем, сам объект при этом может изменяться; в Java нет механизмов, позволяющих сделать произвольный объект неизменным , можно только самому спроектировать свой код так, чтобы объект стал фактически неизменным.
Пустые константы — поля, объявленные как final, которым, однако, не было присвоено начальное значение.
Любую константу надо обязательно инициализировать перед использованием (пустые не исключение).
Неизменные аргументы. Если аргумент метода объявлен с ключевым словом final, то это значит, что метод не может изменить значение, на которое указывает передаваемая ссылка.
Неизменные методы. Если метод объявлен с ключевым словом final, то это значит, что содержимое метода нельзя изменить (даже из производного класса).
Неизменные классы. Если класс объявлен с ключевым словом final, то это означает, что его нельзя использовать в качестве базового.
При этом поля класса могут быть, а могут и не быть неизменными — по вашему выбору . А вот методы неизменного класса будут в любом случае неизменными (т.к. наследование запрещено). Так что прибавление слова final к методам в таком классе ничего не изменит).
Контроль над доступом(сокрытие реализации).
Помещение данных и методов в классы в комбинации с сокрытием реализации часто называют инкапсуляцией.
Причины сокрытия реализации:
1) позволить программисту-клиенту знать, что он может использовать, а что нет.
2)разделение интерфейса и реализации. Это позволит Вам изменять все, что не объявлено как public (члены с доступом в пределах пакета, protected и private), не нарушая работоспособности изменений клиентского кода) .