Классы-оболочки и generics
Java — полностью объектно-ориентированный язык. Это означает, что все, что только можно, в Java представлено объектами.
Восемь примитивных типов нарушают это правило. Они оставлены в Java не только из-за многолетней привычки к числам и символам. Арифметические действия удобнее и быстрее производить с обычными числами, а не с объектами классов, которые требуют много ресурсов от компьютера.
Но и для этих типов в языке Java есть соответствующие классы — классы-оболочки (wrapper) примитивных типов. Конечно, они предназначены не для вычислений, а для действий, типичных при работе с классами, — создания объектов, преобразования типов объектов, получения численных значений объектов в разных формах и передачи объектов в методы по ссылке.
На рис. 4.1 показана одна из ветвей иерархии классов Java. Для каждого примитивного типа в пакете j ava. lang есть соответствующий класс. Числовые классы имеют общего предка — абстрактный класс Number, в котором описаны шесть методов, возвращающих числовое значение, содержащееся в классе, приведенное к соответствующему примитивному типу: byteValue(), doubleValue(), floatValue(), intValue(), longVaiue (), shortValue (). Эти методы переопределены в каждом из шести числовых классов-оболочек Byte, Short, Integer, Long, Float и Double. Имена классов-оболочек, за исключением класса Integer, совпадают с именами соответствующих примитивных типов, но начинаются с заглавной буквы.
Помимо метода сравнения объектов equals(), переопределенного из класса Object, все описанные в этой главе числовые классы, класс Character и класс Boolean имеют метод
Object—р Number-
- Boolean
-Character
-Class
? — BigDecimal —Blglnteger
— Byte
— Double —Float —Integer
— Long
— Short
LCharacter.Subset-i— InputSubset
Character.UnicodeBlock
Рис. 4.1. Классы примитивных типов
compareTo (), сравнивающий числовое значение, символ или булево значение, содержащееся в данном объекте, с числовым значением объекта — аргументом метода compareTo (). В результате работы метода получается целое значение:
? 0, если сравниваемые значения равны;
? отрицательное число (-1), если числовое значение в данном объекте меньше, чем в объекте-аргументе или, для класса Boolean, в данном объекте false, а в аргументе — true;
? положительное число (+1), если числовое значение в данном объекте больше числового значения, содержащегося в аргументе или в данном объекте true, а в аргументе — false.
В каждом из этих классов есть статический метод
int compare(xxx a, xxx b);
который сравнивает значения двух чисел, символов или логических переменных a и b, заданных простыми типами boolean, byte, short, char, int, long, float, double, так же, как и метод compareTo (), и возвращает те же значения.
Еще один полезный статический метод
Xxx valueOf(xxx a);
в котором xxx — это один из простых типов boolean, byte, short, char, int, long, float, double, возвращает объект соответствующего типа. Документация настоятельно рекомендует применять этот метод для создания объектов из простых типов, а не конструктор соответствующего класса.
Что полезного можно найти в классах-оболочках?
Числовые классы
В каждом из шести числовых классов-оболочек есть статические методы преобразования строки символов типа String, представляющей число, в соответствующий примитивный
тип: Byte.parseByte(), Double.parseDouble(), Float.parseFloat(), Integer.parseInt(), Long.parseLong(), Short.parseShort ( ). Исходная строка типа String, как всегда в статических методах, служит параметром метода. Эти методы полезны при вводе данных в поля ввода, обработке аргументов командной строки, т. е. всюду, где числа представляются строками символов, состоящими из цифр со знаками плюс или минус и десятичной точкой.
В каждом из этих классов есть статические константы MAX_VALUE и MIN_VALUE, показывающие диапазон числовых значений соответствующих примитивных типов. В классах
Double и Float есть еще константы POSITIVE_INFINITY, NEGATIVE_INFINITY, NaN, о которых шла речь в главе 1, и логические методы проверки isNaN ( ), isInfinite ( ).
Если вы хорошо знаете двоичное представление вещественных чисел, то можете воспользоваться статическими методами floatToIntBits ( ) и doubleToLongBits ( ), представляющими последовательность битов, из которых состоит двоичное представление вещественного числа, в виде целого числа типа int или long соответственно. Исходное вещественное число задается как аргумент метода. Получив целочисленное представление, вы можете изменить отдельные биты получившегося целого числа побитовыми операциями и преобразовать измененное целое число обратно в вещественное значение методами intBitsToFloat ( ) и longBitsToDouble ().
Статическими методами toBinaryString(), toHexString() и toOctalString() классов Integer и Long можно преобразовать целые значения типов int и long, заданные как аргумент метода, в строку символов, показывающую двоичное, шестнадцатеричное или восьмеричное представление числа.
В листинге 4.1 показано применение этих методов, а рис. 4.2 демонстрирует вывод результатов.
Листинг 4.1. Методы числовых классов
class NumberTest{
public static void main(String[] args){ int i = 0; short sh = 0;
double d = 0;
Integer k1 = Integer.valueOf(55);
Integer k2 = Integer.valueOf(100); Double d1 = Double.valueOf(3.14); try{
i = Integer.parseInt(args[0]); sh = Short.parseShort(args[0]);
d = Double.parseDouble(args[1]); d1 = new Double(args[1]); k1 = new Integer(args[0]); }catch(Exception e){} double x = 1.0 / 0.0; System.out.println("i = " + i); System.out.println("sh = " + sh);
System.out.println("d = " + d);
System.out.println("k1.intValue() = " + k1.intValue()); System.out.println("d1.intValue() = " + d1.intValue());
System.out.println("k1 > k2? " + k1.compareTo(k2));
System.out.println("x = " + x);
System.out.println("x isNaN? " + Double.isNaN(x));
System.out.println("x isInfinite? " + Double.isInfinite(x));
System.out.println("x == Infinity? " + (x == Double.POSITIVE INFINITY)); System.out.println("d = " + Double.doubleToLongBits(d));
System.out.println("i = " + Integer.toBinaryString(i));
System.out.println("i = " + Integer.toHexString(i));
System.out.println("i = " + Integer.toOctalString(i));
}
}
Методы parseInt () и конструкторы классов требуют обработки исключений, поэтому в листинг 4.1 вставлен блок try{}catch(){}. Обработку исключительных ситуаций мы подробно разберем в главе 21.
Начиная с версии Java SE 5 в JDK входит пакет java.util.concurrent.atomic, в котором, в частности, есть классы AtomicInteger и AtomicLong, обеспечивающие изменение числового значения этих классов на уровне машинных команд. Начальное значение задается конструкторами этих классов. Затем методами addAndGet ( ), getAndAdd ( ), incrementAndGet ( ), getAndnIncrement(), decrementAndGet(), getAndDecrement, getAndSet(), set() можно изменять это значение.
Автоматическая упаковка и распаковка типов
В листинге 4.1 объекты числовых классов создавались статическим методом, в котором указывалось числовое значение объекта:
Integer k1 = Integer.valueOf(55);
Это правильно с точки зрения объектно-ориентированного программирования, но утомительно для программиста. Начиная с пятой версии Java, было решено упростить такую запись. Теперь можно писать
Integer k1 = 55;
как будто k1 — простая числовая переменная примитивного типа. Ничего нового в язык Java такая запись не вносит: компилятор, увидев ее, тут же восстановит применение статического метода. Но она облегчает работу программиста, предоставляя ему привычную форму определения переменной. Как говорят, компилятор делает автоматическую упаковку (auto boxing) числового значения в объект. Компилятор может сделать и автоматическую распаковку. После приведенных ранее определений объекта k1 можно написать, например,
int n = k1;
и компилятор извлечет из объекта k1 класса Integer числовое значение 55. Конечно, для этого компилятор обратится к методу intValue () класса Integer, но это незаметно для программиста.
Автоматическая упаковка и распаковка возможна и в методах классов. Рассмотрим простой класс.
class AutoBox{ static int f(Integer value){ return value; // Распаковка.
}
public static void main(String[] args){
Integer n = f(55);
}
}
В методе main() этого примера сначала число 55 приводится к типу параметра метода f() с помощью упаковки. Затем результат работы метода f () упаковывается в объект n класса Integer.
Автоматическую упаковку и распаковку можно использовать в выражениях, написав k1++ или даже (k1 + k2 / k1), но это уже слишком! Представьте себе, сколько упаковок и распаковок вставит компилятор и насколько это замедлит работу программы!
Настраиваемые типы (generics)
Введение в язык Java автоматической упаковки типов позволило определить еще одну новую конструкцию — настраиваемые типы (generics), позволяющие создавать шаблоны классов, интерфейсов и методов. Например, можно записать обобщенный настраиваемый (generic) класс
class MyGenericClass<T>{ private T data;
public MyGenericClass(){}
public MyGenericClass(T data){ this.data = data;
}
public T getData(){ return data;
}
public void setData(T data){ this.data = data;
}
}
в котором есть поле data неопределенного пока типа, обозначенного буквой T. Разумеется, можно написать другую букву или даже идентификатор. Буква T появилась просто как первая буква слова Type.
Перед использованием такого класса-шаблона его надо настроить, задав при обращении к его конструктору определенный тип в угловых скобках. Например:
class MyGenericClassDemo{
public static void main(String[] args){
MyGenericClass<Integer> iMyGen = new MyGenericClass<Integer>(55);
Integer n = iMyGen.getData();
MyGenericClass<Double> dMyGen = new MyGenericClass<Double>(-37.3456);
Double x = dMyGen.getData();
}
}
Если при определении экземпляра настраиваемого класса и слева и справа от знака равенства в угловых скобках записан один и тот же тип, то справа его можно опустить для краткости записи, оставив только пару угловых скобок (так называемый "ромбовидный оператор", "diamond operator"). Используя это новое, введенное в Java 7, сокращение, предыдущий класс можно записать так:
class MyGenericClassDemo{
public static void main(String[] args){
MyGenericClass<Integer> iMyGen = new MyGenericClass<>(55);
Integer n = iMyGen.getData();
MyGenericClass<Double> dMyGen = new MyGenericClass<>(-37.3456);
Double x = dMyGen.getData();
}
}
Рассмотрим более содержательный пример. Пусть нам надо вычислять среднее арифметическое значение нескольких чисел, причем в одном случае это целые числа, в другом — вещественные, в третьем — короткие или, наоборот, длинные целые числа. У среднего значения в любом случае будет тип double. В листинге 4.2 написан один общий класс-шаблон для всех этих случаев.
Листинг 4.2. Настраиваемый класс
class Average<T extends Number>{ T[] data;
public Average(T[] data) { this.data = data; }
public double average(){ double result = 0.0;
for (T t: data) result += t.doubleValue(); return result / data.length;
}
public static void main(String[] args){
Integer[] iArray = {1, 2, 3, 4};
Double[] dArray = {3.4, 5.6, 2.3, 1.24};
Average<Integer> iAver = new Average<>(iArray); System.out.println("int average = " + iAver.average()); Average<Double> dAver = new Average<>(dArray); System.out.println("double average = " + dAver.average());
}
Обратите внимание на то, что в заголовке класса в угловых скобках указано, что тип T — подкласс класса Number. Это сделано потому, что здесь тип T не может быть произвольным. Действительно, в методе average ( ) использован метод doubleValue ( ) класса Number, а это означает, что тип T ограничен классом Number и его подклассами. Кроме того, операции сложения и деления тоже допустимы только для чисел.
Конструкция <T extends SomeClass> ограничивает сверху множество типов, пригодных для настройки параметра T. Таким же образом, написав <t super SomeClass>, можно ограничить снизу тип T только типом SomeClass и его супертипами.
У настраиваемого типа может быть более одного параметра. Они перечисляются в угловых скобках через запятую:
class MyGenericClass2<S, T>{ private S id; private T data;
public MyGenericClass2() {}
public MyGenericClass2(S id, T data){ this.id = id; this.data = data;
}
public S getId(){ return id;
}
public void setId(S data){ this.id = id;
}
public T getData(){ return data;
}
public void setData(T data){ this.data = data;
}
}
Из этих примеров видно, что неопределенные типы S, T могут быть типами параметров конструкторов и типами возвращаемых методами значений. Разумеется, они могут быть типами параметров не только конструкторов, но и любых методов. Более того, типами параметров и типами возвращаемых значений методов могут быть настраиваемые типы. Можно написать метод в такой форме:
public MyGenericClass2<S, T> makeClass2(S id,
MyGenericClass<T> data){ return new MyGenericClass2(id, data.getData());
}
и обратиться к нему так, как показано в листинге 4.3.
Листинг 4.3. Настраиваемые классы — параметры методов
public class MyGenericClass2Demo<S, T>{
public MyGenericClass2<S, T>
makeClass2(S id, MyGenericClass<T> data){
return new MyGenericClass2(id, data.getData());
}
public static void main(String[] args){
MyGenericClass<Double> dMyGen = new MyGenericClass<>(34.456);
MyGenericClass2Demo<Long, Double> d = new MyGenericClass2Demo<>();
MyGenericClass2<Long, Double> ldMyGen2 = d.makeClass2(123456L, dMyGen);
}
}
Шаблон типа (wildcard type)
В предыдущих главах мы часто пользовались тем, что можно определить ссылку типа суперкласса, ссылающуюся на объект подкласса, например:
Number n = new Long(123456L);
Number d = new Double(27.346);
Более того, это свойство распространяется на массивы:
Number[] n = new Long[100];
Можно ли распространить эту возможность на настраиваемые типы? Например, можно ли написать последний оператор листинга 4.3 так:
MyGenericClass2<Number, Number> n = // Сшибка!
d.makeClass2(123456L, dMyGen);
Ответ отрицательный. Из того, что какой-то класс B является подклассом класса A, не следует, что класс g<b> будет подклассом класса g<a>.
Это непривычное обстоятельство вынудило ввести дополнительную конструкцию — шаблон типа (wildcard type), применяемую в процессе настройки типа. Шаблон типа обозначается вопросительным знаком и означает "неизвестный тип" или "произвольный тип". Предыдущий код не вызовет возражений у компилятора, если написать его в таком виде:
MyGenericClass2<? extends Number, ? extends Number> n = // Верно.
d.makeClass2(123456L, dMyGen);
или
MyGenericClass2<Long, ? extends Number> n = // Верно.
d.makeClass2(123456L, dMyGen);
Можно написать даже неограниченный шаблон типа
MyGenericClass2<?, ?> n =
d.makeClass2(123456L, dMyGen);
Такая запись будет почти эквивалентна записи
MyGenericClass2 n =
d.makeClass2(123456L, dMyGen);
за тем исключением, что в первом случае компилятор сделает более строгие проверки.
Кроме записи <? extends Type>, означающей "произвольный подтип типа Type, включая сам тип Type", можно написать выражение <? super Type>, означающее "произвольный супертип типа Type, включая сам тип Type".
Шаблон типа можно использовать в тех местах кода, где настраивается тип, в том числе в параметрах метода:
public MyGenericClass2<S, T> makeClass2(S id,
MyGenericClass<? extends Number> data){
return new MyGenericClass2(id, data.getData());
}
но, поскольку шаблон типа не является типом, его нельзя применять для создания объектов и массивов. Следующие определения неверны:
Average<? extends Number> a = // Ошибка!
new Average<? extends Number>(iArray);
Average<? extends Number>[] a = // Ошибка!
new Average<? extends Number>[10];
Тем не менее при определении массива (но не объекта) можно записать неограниченный шаблон типа:
Average<? extends Number>[] a = // Верно.
new Average<?>[10];
Настраиваемые методы
Настраиваемыми могут быть не только типы, но и методы. Параметры настраиваемого метода (type parameters) указываются в заголовке метода в угловых скобках перед типом возвращаемого значения. Это выглядит так, как показано в листинге 4.4.
Листинг 4.4. Настраиваемый метод
public class MyGenericClass2Demo{
public <S, T> MyGenericClass2<S, T>
makeClass2(S id, MyGenericClass<T> data){
return new MyGenericClass2(id, data.getData());
} public static void main(String[] args){
MyGenericClass<Double> dMyGen = new MyGenericClass(34.456);
MyGenericClass2Demo d =
new MyGenericClass2Demo();
MyGenericClass2<Long, Double> ldMyGen2 = d.makeClass2(123456L, dMyGen);
}
}
Метод makeClass2 () описан в простом, ненастраиваемом, классе MyGenericClass2Demo, и его параметры задаются в угловых скобках <s, t>. Здесь можно записывать ограниченные параметры
public <S extends Number, T extends Number>
MyGenericClass2<S, T> makeClass2(S id, MyGenericClass<T> data){
return new MyGenericClass2(id, data.getData());
}
Как видно из листинга 4.4, специально настраивать метод не нужно, конкретные типы его параметров и возвращаемого значения определяются компилятором по переданным в метод аргументам.
Как вы убедились из приведенных примеров, настраиваемые типы и методы допускают сложную структуру параметров, так же как и вложенные классы. Мы еще не касались вопросов наследования настраиваемых типов, реализации настраиваемых интерфейсов, создания массивов настраиваемых типов. Все эти вопросы подробно рассмотрены на сайте Анжелики Лангер (Angelika Langer), в ее Java Generics FAQ, http:// www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html.