Что хранится в переменной не примитивного типа
В этом руководстве мы рассмотрим плюсы и минусы использования примитивов в Java и соответствующих им классов-оберток.
1. Типы переменных в Java
В языке Java существует два типа переменных: примитивные, например int и boolean, а также ссылочные типы вроде Integer и Boolean (классы-обертки). Для каждого примитивного типа существует, соответствующий ему ссылочный тип.
Классы-обертки являются неизменяемыми: это означает, что после создания объекта его состояние (значение поля value) не может быть изменено; и задекларированы, как final (от этих классов невозможно наследоваться).
Java автоматически производит преобразования между примитивными типами и их обертками:
Integer j = 1; // autoboxing int i = new Integer(1); // unboxing
Процесс преобразования примитивных типов в ссылочные называется автоупаковкой (autoboxing), а обратный ему — автораспаковкой (unboxing).
2. Плюсы и минусы
Решение, переменные каких типов использовать, основывается на необходимой нам производительности приложения, объеме доступной памяти и значений по умолчанию с которыми мы собираемся работать.
2.1 Использование памяти единичными объектами
В зависимости от реализации виртуальной машины, эти значения могут изменяться. Например, в виртуальной машине Oracle значения типа boolean сопоставляются со значениями 0 и 1 типа int (это связано с тем, что в VM нет инструкций для работы с булевыми значениями) и, как результат, занимают в памяти 32 бита.
Переменные примитивных типов хранятся в стеке, позволяя иметь к ним быстрый доступ.
Ссылочные переменные ссылаются на объекты, которые хранятся в куче, к ним мы имеем более медленный доступ (рекомендуем ознакомиться с понятиями Стек и Куча, для лучшего усвоения материала). Ссылочные типы имеют определенные накладные расходы относительно их примитивных аналогов.
Накладные расходы зависят от реализации конкретной JVM. Здесь мы приведем результаты для 64-х битной виртуальной машины со следующими параметрами:
В результате, один экземпляр ссылочного типа на данной JVM занимает 128 бит. За исключением Long и Double, которые занимают 192 бита:
- Boolean — 128 бит
- Byte — 128 бит
- Short, Character — 128 бит
- Integer, Float — 128 бит
- Long, Double — 192 бита
Обратите внимание, переменная типа Boolean занимает в 128 раз больше места чем соответствующий ей примитив, тогда как Integer занимает памяти как 4 int переменные.
2.2 Использование памяти массивами
Более интересно обстоят дела с объемом памяти который занимают массивы, рассматриваемых нами типов.
При создании массивов с различным количеством элементов для каждого типа, мы получаем график:
который демонстрирует как массивы различных типов потребляют память (m) в зависимости от количества содержащихся элементов (e):
- long, double: m = 128 + 64e
- short, char: m = 128 + 64(e/4)
- byte, boolean: m) = 128 + 64(e/8)
- the rest: m = 128 + 64(e/2)
где значения в скобках округляются до ближайшего меньшего.
Это может показаться неожиданным, но массивы примитивных типов long и double занимают больше памяти чем их классы-обертки Long и Double соответственно.
2.3 Производительность
Производительность Java кода, довольно тонкий вопрос, сильно зависящий от аппаратной части устройства на котором он исполняется, от различных оптимизационных процессов, выполняемых компилятором, от конкретной JVM, а также от других процессов, происходящих в операционной системе.
Мы уже упоминали ранее, что переменные примитивных типов живут в стеке, тогда как ссылочные переменные хранят ссылки на объекты расположенные в куче. Это важное свойство, определяющее скорость доступа к данным.
Чтобы продемонстрировать насколько операции над примитивными типами быстрее чем над их классами-обертками, создадим массив из миллиона элементов в котором все элементы одинаковы, кроме последнего. Затем мы попытаемся найти этот элемент и сравним результаты производительности работы массива переменных примитивных типов с массивом ссылочных переменных.
Мы используем известный инструмент тестирования производительности JMH, а результаты операции суммируем в диаграмме:
Как мы видим, даже выполнение такой простой операции требует значительно больше времени для классов-оберток, чем для их примитивных аналогов.
В случае более трудоемких операций, таких как сложение, умножение или деление разница в скорости может значительно увеличиться.
Типы данных
— Т.е. я хотел сказать «Здорова, Амиго». Хочу рассказать тебе о внутреннем устройстве переменных. Ты уже знаешь, что у каждой переменной есть область памяти, привязанная к ней, где эта переменная хранит своё значение.
— Ага. Ты рассказывал это в прошлый раз.
— Отлично. Хорошо, что ты это помнишь. Тогда продолжу.
— Все сложные типы состоят из более простых. Те, в свою очередь, из ещё более простых. Пока, наконец, дело не доходит до самых примитивных, неделимых типов. Их так и называют – примитивные типы. Например, int – это один из примитивных типов, а String – это уже сложный тип, хранящий свои данные в виде таблицы символов (где каждый символ — это примитивный тип — char).
— Очень интересно. Продолжай.
— Сложные типы образуются из простых путём группировки. Такие типы мы называем классами . Когда мы описываем в программе новый класс – это значит, что мы объявляем новый сложный составной тип , данные которого будут или другими сложными типами, или примитивными типами.
public class Person < String name; int age; >
public class Rectangle < int x, y, width, height; >
public class Cat < Person owner; Rectangle territory; int age; String name; >
— Всё пока ещё понятно, как ни странно.
— Т.к. большие (сложные) типы содержат в себе много маленьких (примитивных), то их объекты занимают много памяти. Больше, чем обычные переменные примитивных типов. Иногда намного больше. Присваивание таких переменных выполнялось очень долго и требовало копирования больших объёмов памяти. Поэтому переменные сложных типов хранят в себе не сам объект, а всего лишь ссылку на него! Т.е. четырёхбайтовый адрес. Этого хватает, чтобы можно было обращаться к данным этих объектов. Всю сложность, связанную с этим, берет на себя Java-машина.
— Ничего не понял.
— Мы уже говорили, что переменная – это как коробка. Если ты хочешь сохранить в ней число 13, то ты можешь написать его на листе и положить в коробку.
— Но представь, что тебе надо сохранить в коробку (переменную) что-нибудь побольше. Например, собаку, машину или твоего соседа Васю. Чтобы не пихать в коробку невпихиваемое, можно поступить проще: вместо собаки взять ее фото, вместо машины – ее номер, вместо Васи – его номер телефона.
— Вот мы берем лист бумаги и пишем на нем телефонный номер Васи. Это и будет аналогом ссылки на объект. Если мы достанем из коробки лист с номером Васи, отксерим его и положим в несколько коробок, то количество ссылок на Васю увеличится, но Вася как был один, так и остался. Что, в общем-то, логично.
— Особенность такого хранения данных в том, что ссылок может быть много, а объект – один.
— Очень интересно. Почти понял, кстати. Ответь только еще раз: что будет, если я одной переменной сложного типа присвою другую переменную сложного типа?
— Тогда эти две переменные будут содержать одинаковые адреса. И, значит, изменение данных, хранящихся в одной переменой сложного типа, приведёт к изменению данных, хранящихся в другой . Объект-то, на который они хранят ссылки, реально всего один. А переменных, хранящих на него ссылки, может быть очень много.
— А что хранится в переменных сложных (ссылочных/классовых) типов, пока там ещё нет ссылки на объект? Такое вообще может быть?
— Да, Амиго. Ты опередил меня своим вопросом. Такое может быть. Если в переменной ссылочного (сложного) типа ещё нет ссылки на какой-то объект, то она хранит null – специальную «пустую ссылку». На самом деле, она просто хранит адрес объекта равный 0. Но Java-машина никогда не создаёт объекты с таким адресом, и поэтому всегда знает, что если переменная-ссылка содержит 0, то никакого объекта там нет.
String s; String s = null;
Person person; person = new Person(); person = null;
Cat cat = new Cat(); cat.owner = new Person(); cat.owner.name = "God";
— Я правильно понял? Переменные делятся на два типа: примитивные и ссылочные. Примитивные типы у себя внутри хранят значение, а ссылочные – ссылку на объект. Примитивные типы – это int, char, boolean и ещё немного, а ссылочные типы – это все остальные, и образуются они с помощью классов.
— Абсолютно верно, мальчик мой.
— Раз ты все понял, вот тебе задачи на закрепление материала.
Хранение по ссылке и по значению
Одно значение можно сохранить как есть, но когда их количество неизвестно, то нужен другой подход.
Время чтения: 10 мин
Открыть/закрыть навигацию по статье
- Кратко
- Примитивные типы данных
- Ссылочные типы данных
- Мутации и неизменяемость
- Аргументы функций
- Егор Огарков советует
- Каким будет значение определённого свойства объекта?
Обновлено 5 августа 2022
Кратко
Скопировать ссылку «Кратко» Скопировано
Для хранения различных значений в переменных мы используем разные типы данных. Однако хранятся эти значения по-разному. Примитивные значения (например, числа или строки) хранятся в переменной как есть, а объекты, массивы и функции — по ссылке на место в памяти.
Представим ситуацию, когда у вас в руках есть ложка из набора, и вы кладёте её в какой-то ящик. Такой простой метафорой можно описать присвоение значения в переменную, если представить ящик как переменную, а ложку как значение. Таким образом можно определить и хранение по значению. В следующий раз, когда вы захотите взять ложку, вы можете открыть тот же самый ящик и получить это значение-ложку.
Теперь представим, что у нас есть другая ложка — это специальная ложка, удобная, но при этом никто не знает, какого она размера и как её правильно хранить. Но для удобства вам хотелось так же использовать ящик. Поэтому, когда вы открыли этот же ящик, то там уже не лежит эта ложка, зато находится записка о том, где эту ложку можно найти.
В итоге, чтобы получить ложку, нужно обратиться по этому «адресу». Предположим, что теперь все такие ложки лежат в специальной «ложечной», которая может их вместить, и только оттуда их можно достать. А потому, чтобы получить ложку вам нужно обратиться по данному адресу. Аналогичным образом мы можем положить записки с тем же адресом и в другие ящики, чтобы каждый, кто обращался к ящику знал где найти ложку. Теперь ваша ложка хранится по ссылке.
В чем же фундаментальное отличие?
Отличий несколько, некоторые могут приводить к неприятным последствиями в нашем коде.
То, как будут храниться данные, жёстко связано с типом данных. Нельзя заставить значение примитивного типа храниться по ссылке, и наоборот.
Для того чтобы понять, как хранятся разные типы данных, заглянем в память компьютера.
Примитивные типы данных
Скопировать ссылку «Примитивные типы данных» Скопировано
Когда мы объявляем переменную и сохраняем в неё примитивное значение, то в память записывается какое-то количество байт, которое описывает это значение. Таким образом можно сказать, что наша переменная уже сразу содержит эти байты.
const seven = 7 // 0b0111const eight = 8 // 0b1000
const seven = 7 // 0b0111 const eight = 8 // 0b1000
Если присвоить какое-то значение переменной в другую, то мы просто скопируем это же количество байт в новое место.
const sevenAgain = seven // 0b0111
const sevenAgain = seven // 0b0111
В итоге все наши переменные можно схематически отобразить таким образом:
Когда мы сравниваем два значения, то у нас по сути произойдёт побайтовое сравнение этих величин.
console.log(seven === sevenAgain)// true
console.log(seven === sevenAgain) // true
console.log(seven === eight)// false
console.log(seven === eight) // false
Из-за того, что все примитивные значения хранятся в небольшом и фиксированном количестве байт, операции над ними выполнять несложно. Такие типы данных называют примитивными. В них входят числа ( number ), строки ( string ), булевы ( boolean ), а так же специальные значения null и undefined .
Ссылочные типы данных
Скопировать ссылку «Ссылочные типы данных» Скопировано
С объектами и другими сложными данными дела обстоят сложнее из-за того, что мы не знаем, какое количество памяти для них понадобится. Во время работы с такой структурой компьютеру необходимо следить за тем, сколько памяти уже есть, сколько понадобится, и выделять новую. Работать с такими данными сложнее. Для этого компьютер отдаёт нам ссылку на место, где данные хранятся, и самостоятельно будет работать с ними по инструкциям, которые мы ему даём. Таким образом в переменную мы получаем лишь ссылку на данные.
const myData = <>
const myData = >
Обратите внимание, что направление стрелки поменялось. Так мы обозначим, что наша переменная ссылается на участок памяти.
☝️ Если сейчас присвоить значение из my Data в другую переменную, то мы скопируем ссылку, а не само значение.
const yourData = myData
const yourData = myData
Такой тип данных называется ссылочным и в него входят объекты, массивы и функции. На самом деле и массивы и функции все они так же являются объектами, но это другая история.
Можно ли в таком случае рассчитывать, что значения будут равными? Конечно, можно! В этом случае сравниваться будут ссылки на объект, а не их содержимое. Потому, если обе переменных указываются на одно и то же, смело можно сказать, что значения равны.
const data = <>const anotherData = data console.log(data === anotherData)// true
const data = > const anotherData = data console.log(data === anotherData) // true
И не стоит забывать, что никакого сравнения по значениям не будет, даже если мы создадим абсолютно одинаковые объекты.
const cat = const dog = // Странно ожидать равность кошки и собаки ¯\_(ツ)_/¯ но теперь мы знаем причинуconsole.log(cat === dog)// false
const cat = name: 'Феликс' > const dog = name: 'Феликс' > // Странно ожидать равность кошки и собаки ¯\_(ツ)_/¯ но теперь мы знаем причину console.log(cat === dog) // false
Однако факт того, что несколько переменных могут ссылаться на один и тот же объект означает в себе и некоторые другие особенности. Если кто-то из двух владельцев ссылки будет изменять объект, то изменения отразятся на всех.
yourData.name = 'Саша'console.log(myData)// myData.name = 'Михаил'console.log(yourData)//
yourData.name = 'Саша' console.log(myData) // myData.name = 'Михаил' console.log(yourData) //
Эта особенность часто становится причиной ошибок при работе со ссылочными типами данных, т.к можно легко забыть или даже не знать, что же ещё ссылается на тот же объект.
Если переменная потеряет ссылку на объект, то изменения уже не будут на него влиять
let user = const admin = user // Переопределение никак не повлияет на admin, потому что мы создали новый объектuser = console.log(admin) // admin.isAdmin = true console.log(user) // console.log(admin) //
let user = name: 'Анна', age: 21 > const admin = user // Переопределение никак не повлияет на admin, потому что мы создали новый объект user = name: 'Иван' > console.log(admin) // admin.isAdmin = true console.log(user) // console.log(admin) //
Мутации и неизменяемость
Скопировать ссылку «Мутации и неизменяемость» Скопировано
Изменение значений у полей объекта, добавление или удаление их отразится на всех, кто владеет ссылкой на этот объект. Такие операции называют мутациями. В современных веб-разработке мутаций стараются избегать, потому что мутирование объектов может приводить к ошибкам, которые очень трудно отследить. Однако если мы твердо уверены, что объект нигде более не используется или чётко контролируем ситуацию, то изменение объекта напрямую гораздо проще.
Если нужно безопасно модифицировать объект, то для начала придётся его скопировать. Скопировать объект можно двумя способами: через Object . assign ( ) или используя спред-синтаксис . . .
const admin = name: 'Анна', age: 21, isAdmin: true,> // Чтобы скопировать через Object.assign() нужно передать пустой объектconst adminCopy = Object.assign(<>, admin) const anotherCopy = . admin,>
const admin = name: 'Анна', age: 21, isAdmin: true, > // Чтобы скопировать через Object.assign() нужно передать пустой объект const adminCopy = Object.assign(>, admin) const anotherCopy = . admin, >
Таким образом будет создана совсем новая сущность, которая будет содержать ровно те же значения. Любые изменения в новом объекте уже не затронут предыдущий.
anotherCopy.age = 30anotherCopy.isAdmin = false console.log(anotherCopy)// console.log(admin)//
anotherCopy.age = 30 anotherCopy.isAdmin = false console.log(anotherCopy) // console.log(admin) //
Здесь стоит внести важную оговорку о вложенных объектах. При копировании объекта указанным способом копируются только поля верхней вложенности (сработает поверхностное копирование). Любые вложенные объекты копируются по ссылке. Их изменение затронет и первоисточник:
const original = b: c: 1, >,> const copy = copy.b.c = 2 // Тоже изменился!console.log(original)// < b: < c: 2 >>
const original = b: c: 1, >, > const copy = . original > copy.b.c = 2 // Тоже изменился! console.log(original) // < b: < c: 2 >>
Изменения можно так же внести при копировании.
const cat = name: 'Феликс', color: 'чёрный', isHomeless: false,> const catInBoots = . cat, name: 'Пушок', hasBoots: true,> console.log(catInBoots)// const redCat = Object.assign(cat, < color: 'рыжий', name: 'Борис' >) console.log(redCat)//
const cat = name: 'Феликс', color: 'чёрный', isHomeless: false, > const catInBoots = . cat, name: 'Пушок', hasBoots: true, > console.log(catInBoots) // const redCat = Object.assign(cat, color: 'рыжий', name: 'Борис' >) console.log(redCat) //
Если каждый раз создавать объект, когда мы вносим изменения, то такие объекты называют иммутабельными (immutable) или неизменяемыми. Результатом любой модификации такого объекта всегда должен быть новый объект, при этом старый никак не изменится.
С массивами, кстати, ситуация точно такая же — если изменять содержимое, то изменения отразятся на всех владельцев ссылки. Для копирования массивов, кроме оператора троеточия, можно использовать метод массива slice ( ) . Методы map ( ) и filter ( ) — они тоже создают новый массив. Причём некоторые другие методы (например sort ( ) , splice ( ) ) при использовании мутируют исходный массив, потому использовать их стоит с осторожностью. Подробнее о том, какой метод мутирует массив можно найти на Does It Mutate.
Очевидным минусом использования иммутабельности может быть большее использование памяти, но в реалиях современной разработки это часто не бывает проблемой, учитывая те плюсы, которые мы получаем.
Аргументы функций
Скопировать ссылку «Аргументы функций» Скопировано
Про тип данных стоит помнить особенно внимательно при использовании функций. Когда мы используем значение как аргумент функции, то все особенности его типа данных сохраняются:
- При передаче примитивного типа данных, его значение копируется в аргумент.
- При использовании ссылочного типа данных копируется ссылка. Все изменения в объекте, который был передан в качестве аргумента, будут видны всем, кто владеет ссылкой:
const member = function makeAdmin(user) user.isAdmin = true return user> const admin = makeAdmin(member) console.log(admin)// console.log(member)// // Это один и тот же объектconsole.log(admin === member)// true
const member = id: '123', name: 'Иван' > function makeAdmin(user) user.isAdmin = true return user > const admin = makeAdmin(member) console.log(admin) // console.log(member) // // Это один и тот же объект console.log(admin === member) // true
Заключение
Скопировать ссылку «Заключение» Скопировано
Итак, что мы узнали?
- Примитивные типы данных (числа, булевы и строки) хранятся и сравниваются по значению. Можно безопасно менять значение переменной и не бояться, что изменится что-то ещё
- Ссылочные типы данных (объекты, массивы) хранятся и сравниваются по ссылке. При этом при сравнении будет учитываться именно факт того, что две переменные ссылаются на один и тот же объект. Даже если два объекта содержат идентичные значения это ни на что не повлияет
- Изменения внутри объекта будут видны всем у кого есть ссылка на этот объект. Прямое изменение данных объекта называется мутирование. Лучше стараться избегать мутации объекта, т.к это может приводить к неочевидным ошибкам
- Чтобы безопасно менять ссылочный тип данных его необходимо предварительно скопировать. Таким образом будет создана другая ссылка и любые изменения не затронут старый объект
На практике
Скопировать ссылку «На практике» Скопировано
Егор Огарков советует
Скопировать ссылку «Егор Огарков советует» Скопировано
При копировании можно изменить и добавить поля, но вот удалить без мутации нельзя
const dog = name: 'Барбос', color: 'чёрный',> const puppy = . dog, // Можно выставить значение undefined, но это не удаление color: undefined,> // А это удалит поле, хоть delete считается мутированием// Но использование его на копии изменит только puppy, dog не будет измененdelete puppy.color
const dog = name: 'Барбос', color: 'чёрный', > const puppy = . dog, // Можно выставить значение undefined, но это не удаление color: undefined, > // А это удалит поле, хоть delete считается мутированием // Но использование его на копии изменит только puppy, dog не будет изменен delete puppy.color
Популярные в веб-разработке библиотеки React и Redux сильно завязаны на иммутабельности данных и практически построены на этом. Подробнее об этом подходе читайте в статье «Организация потоков данных».
На собеседовании
Скопировать ссылку «На собеседовании» Скопировано
Примитивные типы в Java
В Java есть 8 базовых примитивных типов. Примитивными их называют потому, что значения этих типов не являются объектами и хранятся прямо внутри переменных.
Вот таблица, которая содержит краткую информацию по этим типам:
Тип Размер,
байтДиапазон значений Значение по умолчанию Описание byte 1 -128 .. 127 0 Самое маленькое целое — один байт short 2 -32,768 .. 32,767 0 Короткое целое, два байта int 4 -2*10 9 .. 2*10 9 0 Целое число, 4 байта long 8 -9*10 18 .. 9*10 18 0L Длинное целое, 8 байт float 4 -10 38 .. 10 38 0.0f Дробное, 4 байта double 8 -10 308 .. 10 308 0.0d Дробное, двойной длины, 8 байт boolean 1 true , false false Логический тип (только true & false ) char 2 0 .. 65,535 ‘\u0000’ Символы, 2 байта, все больше 0 Значение по умолчанию
Кстати, важный нюанс. Если вы объявили переменную-класса (поле класса) или статическую переменную-класса, и не присвоили ей никакого значения, она инициализируется значением по умолчанию . Список таких значений приведен в таблице.
Локальные переменные методов не имеют значений по умолчанию, и если вы не присвоили такой переменной никакого значения, она считается не инициализированной и использовать ее значение нельзя.
Но давайте вернемся к примитивным типам и рассмотрим их подробнее
2. Целые типы
В языке Java аж 4 целых типа: byte , short , int и long . Они отличаются размером и диапазоном значений, которые могут хранить.
Тип int
Самым часто используемым является тип int . Его название происходит от Int eger (целый). Все целочисленные литералы в коде имеют тип int (если в конце числа не указана буква L , F или D ).
Переменные этого типа могут принимать значение от -2,147,483,648 до +2,147,483,647 .
Это достаточно много и хватает почти для всех случаев жизни. Почти все функции, которые возвращают число, возвращают число типа int .
int n = "Строка".length();
String[] array = ; int n = array.length;
Тип short
Тип short получил свое название от short int . Его еще называют короткое целое . В отличие от типа int , его длина всего два байта и возможный диапазон значений от -32,768 до +32,767 .
То есть в нем даже число миллион не сохранишь. Даже 50 тысяч. Это самый редко используемый целочисленный тип в Java. В основном его используют, когда хотят сэкономить на памяти.
Допустим, у вас ситуация, когда заранее известно, что значения с которыми вы работаете не превышает 30 тысяч, и таких значений миллионы.
Например, вы пишете приложение, которое обрабатывает картинки сверхвысокой четкости: на один цвет приходится 10 бит. А точек у вас в картинке — миллион. И вот тут уже играет роль, используете вы тип int или short .
Тип long
Этот тип получил свое название от long int — его еще называют длинное целое . В отличие от типа int , у него просто гигантский диапазон значений: от -9*10 18 до +9*10 18
Почему же он не является основным целым типом?
Все дело в том, что Java появилась еще в середине 90-х, когда большинство компьютеров были 32-х разрядными. А это значило, что все процессоры были заточены под работу с числами из 32-х бит. С целыми числами из 64-х бит процессоры работать уже умели, но операции с ними были медленнее.
Поэтому программисты разумно решили сделать стандартным целым типом тип int , ну а тип long использовать только тогда, когда без него действительно не обойтись.
Тип byte
Это самый маленький целочисленный тип в Java, но далеко не самый редко используемый. Его название совпадает со словом byte — минимальная адресуемая ячейка памяти в Java.
Размер допустимых значений типа byte не так уж велик: от -128 до +127 . Но не в этом его сила. Тип byte чаще всего используется, когда нужно хранить в памяти большой блок обезличенных данных. Массив типа byte просто идеально подходит для этих целей.
Например, вам нужно куда-то скопировать файл.
Вам не нужно обрабатывать содержимое файла: вы просто хотите создать область памяти (буфер), скопировать в нее содержимое файла, а затем записать эти данные из буфера в другой файл. Массив типа byte — то, что нужно для этих целей.
Тем более, что в переменной-типа-массив хранится только ссылка на область памяти. При передаче значения этой переменной в какой-то метод произойдет только передача адреса в памяти, а сам блок памяти копироваться не будет.
byte[] buffer = new byte[1024*1024]; FileInputStream sourceFile = new FileInputStream("c:\\data.txt"); FileOutputStream destFile = new FileOutputStream("c:\\output.txt"); while (true) < int size = sourceFile.read(buffer); // читаем данные из файла в буфер destFile.write(buffer, 0, size); // записываем данные из буфера в файл // прекращаем копирование, если буфер заполнен не полностью if (size < buffer.length) break; >sourceFile.close(); destFile.close();
3. Вещественные типы
Среди примитивных типов также есть два вещественных. Хотя это не совсем точное название. Официально они называются числа с плавающей точкой — floating point numbers . Название происходит из стандарта, когда целую и дробную часть числа разделяет точка (а не запятая).
В каждой стране свои стандарты для записи чисел (внезапно!).
Многие из нас привыкли писать точки для разделения тысяч и запятую для отделения дробной части: например, миллион целых и 153 тысячных мы бы записали так 1.000.000,153 . А вот в США, где жили создатели Java, принят другой стандарт: 1,000,000.153
В Java есть два примитивных типа с плавающей точкой: double и float .
Как мы уже говорили ранее, эти типы внутри устроены специфическим образом: фактически внутри каждой переменной этих типов находится не одно число, а два:
Например, дробное число 987654.321 можно представить как 0. 987654321 *10 6 . Поэтому в памяти оно будет представлено как два числа 987654321 ( мантисса — значащая часть числа) и 6 ( экспонента — степень десятки)
Тип float
Само название типа float происходит от float ing point number . Размер этого типа совсем небольшой — всего 4 байта (32 бита), но он может хранить значения от -3.4*10 38 до 3.4*10 38 . Под мантиссу отдается 24 бита, под экспоненту — 8 бит. Этот тип способен хранить всего 8 значащих цифр.
Такой подход позволяет хранить гораздо большие числа, чем int , используя все те же 4 байта. Но при этом мы жертвуем точностью. Часть памяти расходуется на хранение мантиссы, поэтому такие числа хранят всего 6-7 знаков после запятой, остальные отбрасываются.
float a = (float) 123.456789;
float a = (float) 12345.9999;
float a = (float) -123.456789E-2;
Как видите, основной недостаток этого типа — очень маленькое количество значащих цифр и потеря точности уже в восьмой цифре. Поэтому тип float не сильно популярен среди Java-программистов.
Тип double
Тип double является стандартным типом с плавающей точкой. Его название происходит от double floating point . Его еще называют числом с плавающей точкой двойной точности . Все вещественные литералы по умолчанию имеют тип double .
Этот тип занимает 8 байт памяти (64 бита) и может хранить значения от -1.7*10 308 до 1.7*10 308 . Важным моментом является то, что под его мантиссу отводится 53 бита, а остальные 11 – под экспоненту.
Это позволяет хранить 15-17 значащих цифр.
double a = 1234567890.1234567890;
double a = 1234567890.1234512345;
double a = 1234567890.1357913579;
Такая точность, особенно в сравнении с типом float , является определяющей: 99% всех операций с вещественными числами выполняются с типом double .
Под экспоненту выделяется 11 бит, что позволяет хранить степень десятки от -323 до +308 (степень двойки — от -1024 до +1023 ). Тип double легко может хранить число с сотней нулей после запятой:
double a = 2E-300 * 3E+302
4. Бесконечность
Числа с плавающей точкой обладают еще одной интересной особенностью: они позволяют хранить специальное значение, обозначающее бесконечность. Причем может быть положительная бесконечность и отрицательная бесконечность.
System.out.println( 100.0 / 0.0 );
Infinity
System.out.println( -100.0 / 0.0 );
-Infinity
double a = 1d / 0d; double b = a * 10; double c = b - 100;
a == Infinity b == Infinity c == Infinity
Если бесконечность умножить на число, получится бесконечность. Если к бесконечности добавить число, получится бесконечность. Очень удобно.
Не число ( NaN )
Любые операции с бесконечностью дают бесконечность. В целом да, но не все.
Числа с плавающей точкой могут хранить еще одно специальное значение — NaN . Это сокращение от N ot a N umber (не число).
В математике, если разделить бесконечность на бесконечность, должна возникнуть неопределенность.
Ну, а в Java, если разделить бесконечность на бесконечность, будет NaN .
System.out.println(0.0 / 0.0);
double infinity = 1d / 0d; System.out.println(infinity / infinity);
double a = 0.0 / 0.0; double b = a * 10; double c = b - 100; double d = a + infinity;
a == NaN b == NaN c == NaN d == NaN
Любая операция с NaN дает NaN .
5. Тип char
Среди примитивных типов в Java есть еще один, который заслуживает особого внимания — тип char . Его название происходит от слова Char acter , а сам тип используется для того, чтобы хранить символы.
А ведь символы — это как раз то, из чего состоят строки: каждая строка содержит в себе массив символов.
Но еще интереснее тот факт, что тип char — это и числовой тип тоже ! Так сказать, тип двойного назначения.
Все дело в том, что на самом деле тип char хранит не символы, а коды символов из кодировки Unicode. Каждому символу соответствует число — числовой код символа.
Каждая переменная типа char занимает в памяти два байта (как и тип short ). Но в отличие от типа short , целочисленный тип char — беззнаковый, и может хранить значения от 0 до 65,535 .
Тип char — гибридный тип. Его значения можно интерпретировать и как числа (их можно складывать и умножать), и как символы. Так было сделано потому, что хоть символы и имеют визуальное представление, для компьютера они в первую очередь просто числа. И работать с ними как с числами гораздо удобнее.
Unicode
Unicode — это специальная таблица (кодировка), которая содержит все символы мира. И у каждого символа есть свой номер. Выглядит она примерно так:
Присвоить значение переменной типа char можно разными способами.
char a = 'A';
char a = 65;
char a = 0x41;
char a = 0x0041;
char a = '\u0041';
Чаще всего просто указывают символ в кавычках (как в первой строке таблицы). Хотя популярен и последний способ. Его преимущество в том, что его можно использовать в строках.
И как мы говорили, тип char — это и целочисленный тип тоже, поэтому можно написать так:
char a = 'A'; a++; System.out.println(a);
Работа с типом char
Каждый символ char — это в первую очередь число (код символа), а потом уже символ. Зная код символа, всегда можно получить его в программе. Пример:
char c = (char) 1128; System.out.println(c);
Стандартные коды
Вот самые известные коды символов:
Символы Коды 0 , 1 , 2 , . 9 48 , 49 , 50 , . 57 a , b , c , . z 97 , 98 , 99 , . 122 A , B , C , . Z 65 , 66 , 67 , . 90 6. Тип boolean
И последний примитивный тип — это boolean .
Как вы уже знаете, он может принимать только два значения: true и false .
Собственно, все, что можно знать об этом типе, вы уже знаете.
- Мутации и неизменяемость