Оболочечные классы. Упаковка (boxing) и распаковка (unboxing)

В ряде случаев вместо значения примитивного типа требуется объект. Например, для работы со списками объектов. Это связано с тем, что работа с объектами в Java может быть унифицирована, поскольку все классы Java являются наследниками класса Object, а для примитивных типов этого сделать нельзя.

Для таких целей в Java каждому примитивному типу сопоставляется объектный тип, то есть класс. Такие классы называются оболочечными (class wrappers). В общем случае они имеют те же имена, что и примитивные типы, но начинающиеся не со строчной, а с заглавной буквы. Исключение почему-то составляют типы int и char, для которых имена оболочечных классов Integer и Character.

 

Примитивный тип Оболочечный класс
byte Byte
short Short
char Character
int Integer
long Long
float Float
double Double

 

Внимание! Класс Character несколько отличается от остальных оболочечных числовых классов потому, что тип char “не вполне числовой”.

Основное назначение оболочечных классов – создание объектов, являющихся оболочками над значениями примитивных типов. Процесс создание такого объекта (“коробки” - box) из значения примитивного типа называется упаковкой (boxing), а обратное преобразование из объекта в величину примитивного типа – распаковкой (unboxing). Оболочечные объекты (“обёртки” для значения примитивного типа) хранят это значение в поле соответствующего примитивного типа, доступном по чтению с помощью функции имяТипаValue(). Например, метода byteValue() для объекта типа Byte. Но во многих случаях можно вообще не обращать внимания на отличие переменных с типом оболочечных классов от переменных примитивных типов, так как упаковка и распаковка при подстановке такого объекта в выражение происходит автоматически, и объект оболочечного типа в этих случаях внешне ведёт себя как число. Таким образом, если нам необходимо хранить в объекте числовое значение, следует создать объект соответствующего оболочечного типа.

Например, возможны такие фрагменты кода:

Integer obj1=10;

int i1= obj1*2;

Byte b=1;

obj1=i1/10;

b=2;

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

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

Byte.MIN_VALUE, Byte.MAX_VALUE, Float.MIN_VALUE, Float.MAX_VALUE, Double.MIN_VALUE, Double.MAX_VALUE и т.п.

В оболочечных классах также имеются методы классов, то есть такие, которые могут работать в отсутствии объекта соответствующего типа – для их вызова можно пользоваться именем типа. Например, как мы уже знаем, имеются методы

Byte.parseByte(строка)

Short.parseShort(строка)

Integer.parseInt(строка)

Long.parseLong(строка)

Float.parseFloat(строка)

Double.parseDouble(строка)

Они преобразуют строку в число соответствующего типа.

Вызовы

Byte.valueOf(строка)

Short.valueOf(строка)

Integer.valueOf(строка)

Long.valueOf (строка)

Float.valueOf (строка)

Double.valueOf (строка)

аналогичны им, но возвращают не числовые значения, а объекты соответствующих оболочечных типов.

Примеры использования оболочечных классов:

int n1=Integer.MAX_VALUE;

double d1= Double.MIN_VALUE;

Отметим, что присваивание

double d2= Double.parseDouble(jTextField1.getText());

будет работать совершенно так же, как

double d2= Double.valueOf(jTextField1.getText());

несмотря на то, что во втором случае методом valueOf создаётся объект оболочечного типа Double. Поскольку в левой части присваивания стоит переменная типа double, происходит автоматическая распаковка, и переменной d2 присваивается распакованное значение. Сам объект при этом становится мусором – программная связь с ним теряется, и он через некоторое время удаляется из памяти системой сборки мусора. В данном случае ни быстродействие, ни объём памяти некритичны, поскольку операции взаимодействия с пользователем по компьютерным меркам очень медленные, а один объект оболочечного типа занимает пренебрежимо мало места в памяти (около сотни байт). Так что с потерями ресурсов в этом случае можно не считаться, обращая внимание только на читаемость текста программы. Поэтому автор предпочитает второй вариант присваивания: хотя он и “неоптимальный” по затрате ресурсов, но более читаем.

Приоритет операторов

При вычислении выражений важен приоритет операторов. Для операторов сложения, вычитания, умножения и деления он “естественный”: умножение и деление обладают одинаковым наиболее высоким приоритетом, а сложение и вычитание – одинаковым приоритетом, который ниже. Таким образом, например,

a*b/c+d

это то же, что

( (a*b)/c )+d

Круглые скобки позволяют группировать элементы выражений, при этом выражение в скобках вычисляется до того, как участвует в вычислении остальной части выражения. То есть скобки обеспечивают больший приоритет, чем все остальные операторы. Поэтому

(a+b)*c будет вычисляться так: сначала вычислится сумма a+b, после чего полученный результат будет умножен на значение c.

Кроме перечисленных в Java имеется большое количество других правил, определяющих приоритеты различных операторов. Автор считает их изучение не только нецелесообразным, но даже вредным: программу следует писать так, чтобы все последовательности действий были очевидны и не могли вызвать сложностей в понимании текста программы и привести к логической ошибке. Поэтому следует расставлять скобки даже в тех случаях, когда они теоретически не нужны, но делают очевидной последовательность действий. Отметим, такие действия часто помогают заодно решить гораздо более сложные проблемы, связанные с арифметическим переполнением.

Далее в справочных целях приведена таблица приоритета операторов. Ей имеет смысл пользоваться в случае анализа плохо написанных программ, когда из текста программы неясна последовательность операторов.

 

Приори-тет Группа операторов Операторы
высший Постфиксные ( ) [ ] .  
Унарные ++ операнд операнд++ --операнд операнд-- ~ ! + операнд - операнд
Создания объектов и преобразования типа new (тип) операнд  
Мультипликатив-ные * / %  
Аддитивные + -  
Сдвиги битов >> >>> <<  
Отношения > >= < <= instanceof
Эквивалентности == !=  
Побитовое И &  
Побитовое исключающее ИЛИ ^
Побитовое ИЛИ |
Логическое И &&
Логическое ИЛИ ||
Условный ? :
низший Присваивания = Оператор=( +=, -=, *=, /=и т.п. )

 

 

Типы-перечисления (enum)

Иногда требуется использовать элементы, которые не являются ни числами, ни строками, но ведут себя как имена элементов и одновременно обладают порядковыми номерами. Например, названия месяцев или дней недели. В этих случаях используют перечисления. Для задания типа какого-либо перечисления следует написать зарезервированное слово enum (сокращение от enumeration – “перечисление”), после которого имя задаваемого типа, а затем в фигурных скобках через запятую элементы перечисления. В качестве элементов можно использовать любые простые идентификаторы (не содержащие квалификаторов вида имя1.имя2).

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

ИмяТипа.имяЭлемента

У каждого элемента перечисления имеется порядковый номер, соответствующий его положению в наборе - нумерация начинается с нуля. Поэтому первый элемент имеет номер 0, второй элемент – номер 1, и так далее. Имеется функция ordinal(), возвращающая порядковый номер элемета в перечислении. Также имеется функция compareTo, позволяющая сравнивать два элемента перечисления - она возвращает разницу в их порядковых номерах.

Строковое представление значения можно получить с помощью функции name(). Преобразование из строки в значение типа “перечисление” осуществляется с помощью функции класса valueOf, в которую передаётся строковое представление значения.

Если требуется рассматривать элементы перечисления как массив, можно воспользоваться функцией values() – она возвращает массив элементов, к которым можно обращаться по индексу. Формат вызова функции такой: ИмяТипа.values()

Для примера зададим типы-перечисления Monthes (“месяцы”) и Spring (“весна”), соответствующие различным наборам месяцев:

enum Monthes {jan,feb,mar,apr,may,jun,jul,aug,sept,oct,nov,dec};

enum Spring { march, apr, may };

Названия месяцев мы намеренно пишем со строчной буквы для того, чтобы было понятно, что это идентификаторы переменных, а не типы. А имя марта написано по-разному в типах Monthes и Spring для того, чтобы показать независимость их пространств имён.

Объявление переменных типа “перечисление” делается так же, как для всех остальных типов, при этом переменные могут быть как неинициализированы, так и инициализированы при задании:

public Monthes m1 ,m2=Monthes.mar, m3;

- при задании в классе общедоступных полей m1, m2 и m3,

Spring spr1=Spring.apr, spr2;

- при задании в методе локальной переменной или задании в классе поля spr1 с пакетным уровнем доступа.

После чего возможны следующие операторы:

spr2=spr1;

spr1=Spring.may;

System.out.println("Результат сравнения="+spr2.compareTo(Spring.march));

После выполнения этих операторов в консольное окно будет выведен текст

Результат сравнения=1 ,

поскольку в переменной spr2 окажется значение Spring.apr , порядковый номер которого на 1 больше, чем у значения Spring.march , с которым идёт сравнение.

 

Пусть в переменной spr2 хранится значение Spring.may. Порядковый номер значения, хранящегося в переменной, можно получить с помощью вызова spr2.ordinal() . Он возвратит число 2, так как may – третий элемент перечисления (сдвиг на 1 получается из-за того, что нумерация начинается с нуля).

Строковое представление значения, хранящегося в переменной spr2, можно получить с помощью вызова spr2.name() . Он возвратит строку “may” - имя типа в возвращаемое значение не входит.

Если переменная типа “перечисление” не инициализирована, в ней хранится значение null. Поэтому вызов

System.out.println("spr2="+spr2);

осуществлённый до присваивания переменной spr2 значения возвратит строку

spr2=null

А вот попытки вызовов spr2.ordinal() или spr2.name() приведут к возникновению ошибки (исключительной ситуации) с диагностикой

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException

Получение значения типа Spring по номеру, хранящемуся в переменной i, осуществляется так:

spr1=Spring.values()[i];

Преобразование из строки в значение типа Spring будет выглядеть так:

spr1=Spring.valueOf("march");


Краткие итоги по главе 3

ü Величины типа boolean принимают значения true или false.

ü Логические операторы && -“И”, || - “ИЛИ”, ^ - “Исключающее ИЛИ”, ! – “НЕ” применимы к величинам булевского типа. Логические выражения в Java вычисляются в соответствии с укороченным оцениванием.

ü Операторы сравнения применимы к любым величинам a и b одного типа, а также к произвольным числовым величинам a и b, не обязательно имеющим один тип. В качестве оператора сравнения на равенство используется составной символ, состоящий из двух подряд идущих символа равенства “==”.

ü В Java имеются встроенные примитивные целые типы byte, short, int, long и символьный тип char, в некотором смысле также являющийся целочисленным. При этом только тип char беззнаковый, все остальные – знаковые.

ü Для задания в тексте программы численных литерных констант типа long, выходящих за пределы диапазона чисел типа int, после написания числа следует ставить постфикс – букву L.

ü Константами называются именованные ячейки памяти с неизменяемым содержимым. Объявление констант осуществляется в классе, при этом перед именем типа константы ставится комбинация зарезервированных слов public и final.

ü В Java имеется два встроенных примитивных вещественных типа float и double (точнее, типы чисел в формате с плавающей точкой).

ü Математические функции, а также константы “пи” (Math.PI) и “е” (Math.E ) заданы в классе Math, находящемся в пакете java.lang .

ü Целочисленные математические вычисления проводятся на аппаратном уровне только с величинами типа int или long. Для величин типа byte, short или char сначала происходит преобразование в тип int, после чего производится их подстановка в качестве операндов. Если же один из операндов имеет тип long, действия производятся с числами типа long, поскольку второй операнд автоматически преобразуется к этому типу.

ü При работе с вещественными величинами в Java возможна работа на аппаратном уровне только с операндами типов float и double. Если один из операндов имеет тип double, а другой float, действия производятся с числами типа double, поскольку операнд типа float автоматически преобразуется к типу double.

ü Если один из операндов целочисленный, а другой вещественный, сначала идёт преобразование целочисленного операнда к такому же вещественному типу, а потом выполняется оператор.

ü В Java каждому примитивному типу сопоставляется объектный тип, то есть класс. Такие классы называются оболочечными (class wrappers). В общем случае они имеют те же имена, что и примитивные типы, но начинающиеся не со строчной, а с заглавной буквы. Исключение составляют типы int и char, для которых имена оболочечных классов Integer и Character.

ü Основное назначение оболочечных классов – создание объектов, являющихся оболочками над значениями примитивных типов. Процесс создание такого объекта (“коробки” - box) из значения примитивного типа называется упаковкой (boxing), а обратное преобразование из объекта в величину примитивного типа – распаковкой (unboxing). Упаковка и распаковка для числовых классов осуществляется автоматически.

ü В оболочечных классах имеется ряд полезных методов и констант. Например, минимальное по модулю не равное нулю и максимальное значение числового типа можно получить с помощью констант, вызываемых через имя оболочечного типа: Integer.MIN_VALUE, Integer.MAX_VALUE, Float.MIN_VALUE , Float.MAX_VALUE, Double.MIN_VALUE , Double.MAX_VALUE. и т.п.

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

ü Иногда требуется использовать элементы, которые не являются ни числами, ни строками, но ведут себя как имена элементов и одновременно обладают порядковыми номерами. Например, названия месяцев или дней недели. В этих случаях используют перечисления.

Типичные ошибки:

  • Очень часто встречается ошибка, когда вместо сравнения численных значений вида a==b программист пишет a=b.Чаще всего такая ошибка возникает в условных операторах: вместо if(a==b){...} пишут if(a=b){...}. В Java такая ошибка диагностируется на этапе компиляции для всех типов, кроме булевского. Для булевских a и b вызов if(a=b){...} приведёт к тому, что величине a будет присвоено значение b, и результат присваивания возвратится равным этому значению. А диагностики ошибки, к сожалению, выдано не будет.
  • Начинающие программисты, изучавшие ранее язык BASIC, пытаются использовать выражение вида a^bдля возведения в степень, то есть для вычисления выраженияab. В Java для такой операции следует использовать выражение Math.pow(a,b).
  • Программисты, изучавшие ранее C/C++, пытаются использовать логические выражения вида a&bилиa|bдля полной (не укороченной) оценки логических выражений. В Java нет полной (не укороченной) оценки логических выражений, а операторы & и | зарезервированы для арифметических побитовых операций “И” и “ИЛИ”.
  • При использовании оператора instanceof пытаются написать instanceOf.
  • Путают класс java.lang.Math и пакет java.math .
  • Очень часто встречается ошибка, когда забывают, что для величин типа byte, short или char сначала происходит преобразование в тип int, и только после этого производится их подстановка в качестве операндов. Поэтому, в частности, побитовые операции с величинами этих типов дают те же результаты, что и при работе с величинами типа int.
  • Не ставятся скобки, группирующие операнды в длинных выражениях, где стороннему программисту неочевидна последовательность выполнения операндов.
  • Элементы перечисления пытаются задавать как строковые, или же пытаются предварительно задать числовые переменные с именами элементов перечисления.

Задания

  • На основе проекта с графическим пользовательским интерфейсом создать новый проект. В нём для каждого из целочисленных и вещественных типов задать переменные и кнопки с соответствующими надписями. При нажатии на кнопки должны показываться диалоговые панели с сообщением об имени и значении соответствующей переменной.
  • На основе проекта с графическим пользовательским интерфейсом создать новый проект. В нём сделать два пункта ввода, метку и кнопки “Сложить”, “Умножить”, “Разделить”, “sin”. По нажатию на кнопки“Сложить”, “Умножить”, “Разделить” в метку должен выводиться результат. Действия проводить с величинами типа double. По нажатию на кнопку “sin” в метку должен выводиться синус значения, показывавшегося до того в метке.
  • На основе проекта с графическим пользовательским интерфейсом создать новый проект. В нём сделать пункта ввода и радиогруппу с выбором варианта для каждого из целочисленных типов, а также кнопку JButton с надписью “Преобразовать в число”. При выборе соответствующего варианта в пункте ввода должно возникать случайным образом генерируемое число, лежащее в пределах допустимых значений для этого типа. При нажатии на кнопку содержимое пункта ввода должно быть преобразовано в число соответствующего типа, и это значение должно быть показано с помощью диалоговой панели с сообщением.

Работа с выбором вариантов осуществляется следующим образом:

if(jRadioButton1.isSelected() )

оператор1;

if(jRadioButton2.isSelected())

оператор2;

if(jRadioButton3.isSelected())

оператор3;

  • Создать приложение Java с графическим пользовательским интерфейсом. В нём должны быть перечислением Spring (“весна”), в котором перечислены весенние месяцы, и кнопки “ m2=m1” и “Вывести значение m2”. Задать две переменные типа Spring – m1 и m2. Переменную m1 инициализированной, со значением April, переменную m2 – не инициализированной. При нажатии на кнопку “ m2=m1” переменной m2 должно присваиваться значение m1. При нажатии на кнопку “Вывести значение m2” должно выводиться значение переменной m2.