Главная Новости

Java Урок 63: ОБОБЩЕНИЯ, extends и ? в обобщениях


Опубликовано: 01.09.2018

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

Скачать исходники для статьи можно ниже

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

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

Чтобы создать такой класс, можно попробовать что-то вроде этого:

// Stats пытается (безуспешно) создать // обобщенный класс, который вычисляет // среднее значение массива чисел // заданного типа. // Класс содержит ошибку! class Stats<T> { Т[] nums; // nums — это массив элементов типа Т // Передать конструктору ссылку // на массив значений типа Т. Stats(Т[] о) { nums = о; } // Возвращает double во всех случаях. double average() { double sum = 0.0; for(int i=0; i < nums.length; i++){ sum + = nums[i].doubleValue(); // Ошибка!!! } return sum / nums.length; } }

Метод average() класса Stats пытается получить версию типа double каждого числа в массиве nums, вызывая метод doubleValue(). Поскольку все числовые классы, такие как Integer и Double, являются подклассами Number, а класс Number определяет метод doubleValue(), этот метод доступен всем числовым классам-оболочкам.

Проблема в том, что компилятор не имеет возможности узнать, что вы намерены создавать объекты класса Stats, используя только числовые типы.

То есть, когда вы компилируете класс Stats, выдается сообщение об ошибке, свидетельствующее о том, что метод doubleValue() не известен. Чтобы решить эту проблему, вам нужен какой-то способ сообщить компилятору, что вы собираетесь передавать в параметре Т только числовые типы.

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

Чтобы справиться с этой ситуацией, язык Java предлагает ограниченные типы. Когда указывается параметр типа, вы можете создать ограничение сверху, которое объявляет суперкласс, от которого должны быть унаследованы все аргументы типов. Для этого используется ключевое слово extends при указании параметра типа, как показано ниже:

<Т extends суперкласc>

Это означает, что параметр Т может быть заменен только классом суперкласс либо его подклассами.

То есть суперкласс объявляет включающую верхнюю границу.

Вы можете использовать ограничение сверху, чтобы исправить класс Stats,показанный выше, указав класс Number как верхнюю границу используемого параметра типа:

// В этой версии Stats аргумент типа // Т должен быть либо Number, либо классом, // унаследованным от него. class Stats<T extends Number> { T[] nums; // массив Number или подклассов // Передать конструктору ссылку на массив // элементов Number или его подклассов. Stats(T[] о) { nums = о; } // Возвратить double во всех случаях, double average() { double sum = 0.0; for(int i=0; i < nums.length; i++){ sum + = nums[i].doubleValue(); } return sum / nums.length; } } // Демонстрация Stats, class BoundsDemo { public static void main(String args[]) { Integer inums[] = { 1, 2, 3, 4, 5 }; Stats<Integer> iob = new Stats<Integer>(inums); double v = iob.average(); System.out.println("Среднее значение iob равно " + v); Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats<Double> dob = new Stats<Double>(dnums); double w = dob.average(); System.out.println("Среднее значение dob равно " + w); // Это не скомпилируется, потому что String не является // подклассом Number. // String strs[] = { "1", "2", "3", "4", "5" }; // Stats<String> strob = new Stats<String>(strs); // double x = strob.average(); // System.out.println("Среднее значение strob равно " + v); } }

Результат работы этой программы выглядит следующим образом:

Среднее значение iob равно 3.0

Среднее значение dob равно 3.3

Обратите внимание на то, что класс Stats теперь объявлен так:

class Stats<T extends Number> {

Поскольку тип Т теперь ограничен классом Number, компилятор Java знает, что все объекты типа Т могут вызывать метод doubleValue(), так как это метод класса Number. Это уже серьезное преимущество.

Однако в качестве дополнительного бонуса ограничение параметра Т также предотвращает создание нечисловых объектов класса Stats.

Например, если вы попытаетесь убрать комментарии в строках,

находящихся в конце программы, и перекомпилировать ее, то получите ошибку времени компиляции, потому что класс String не является подклассом Number.

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

Более того, такое ограничение может включать как тип класса, так и один или более интерфейсов. В этом случае тип класса должен

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

class Gen<T extends MyClass & Mylnterface> { // ...

Здесь параметр T ограничен классом по имени MyClass и интерфейсом

Mylnterface. То есть любой тип, переданный параметру Т, должен быть подклассом класса MyClass и иметь реализацию интерфейса Mylnterface.

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

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

значения в них содержатся.

Например, если один объект содержит значения типа double 1.0, 2.0 и 3.0, а другой — целочисленные значения 2, 1 и 3, то среднее

значение у них будет одинаково. Один из способов реализации метода sameAvg() — передать ему аргумент класса Stats, а затем сравнивать его среднее значение со средним значением вызывающего объекта, возвращая значение true, если они равны.

Например, необходимо иметь возможность вызывать метод sameAvg(), как показано ниже:

Integer inums[] = { 1, 2 , 3 , 4, 5 }; Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats<Integer> iob = new Stats<Integer>(inums); Stats<Double> dob = new Stats<Double>(dnums); if(iob.sameAvg){ System.out.println("Средние значения одинаковы."); } else{ System.out.println("Средние значения отличаются."); }

Вначале написание метода sameAvg() кажется простой задачей. Поскольку

класс Stats является обобщенным и его метод average() может работать с объектами класса Stats любого типа, кажется, что написание метода sameAvg() не представляет сложности. К сожалению, проблема появляется сразу, как только вы попытаетесь объявить параметр типа Stats. Поскольку Stats — параметризованный тип, какой тип параметра вы укажете для Stats, когда создадите параметр типа Stats?

Сначала вы можете подумать о решении вроде такого, в котором параметр Т будет использоваться как параметр типа:

// Это не сработает! // Определение эквивалентности двух средних значений. boolean sameAvg(Stats<T> ob) { if(average() == ob.average()){ return true; } else { return false; } }

С приведенным выше кодом связана проблема, которая заключается в том, что такой код будет работать только с другим объектом класса Stats, тип которого совпадает с вызывающим объектом.

Например, если вызывающий объект имеет тип Stats , то параметр ob также должен иметь тип Stats .

Он, например, не может применяться для сравнения среднего значения типа Stats со средним значением типа Stats.

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

Чтобы создать обобщенную версию метода sameAvg(), следует использовать

другое средство обобщений Java — шаблоны аргументов. Шаблон аргумента указывается символом ? и представляет собой неизвестный тип. Применение шаблона — единственный способ написать работающий метод sameAvg().

// Определение эквивалентности двух средних значений. // Отметим использование шаблонного символа, Boolean sameAvg(Stats<?> ob) if(average() == ob.average()){ return true; } else { return false; } }

Здесь часть Stats> соответствует любому объекту класса Stats, что позволяет сравнивать между собой средние значения любых двух объектов класса Stats.

Это демонстрируется в следующей программе:

// Использование шаблона, class Stats<T extends Number> { T[] nums; // массив Number или подклассов // Передать конструктору ссылку на массив // типа Number или его подклассов. Stats(T[] о) { nums = о; } // Во всех случаях возвращает double, double average() { double sum = 0.0; for(int i=0; i < nums.length; i++){ sum = sum + nums[i].doubleValue(); } return sum / nums.length; } // Определение эквивалентности двух средних. // Обратите внимание на использование шаблонов, boolean sameAvg(Stats<?> ob) { if(average() == ob.average()) { return true; } else { return false; } } } // Демонстрация шаблона. class WildcardDemo { public static void main(String args[]) { Integer inums[] = { 1, 2 , 3 , 4, 5 }; Stats<Integer> iob = new Stats<Integer>(inums); double v = iob.average(); System.out.println("Среднее для iob равно " + v); Double dnums[] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats<Double> dob = new Stats<Double>(dnums); double w = dob.average(); System.out.println("Среднее для dob равно " + w); Float fnums[] = { 1.0F, 2.OF, 3.0F, 4.OF, 5.OF }; Stats<Float> fob = new Stats<Float>(fnums); double x = fob.average(); System.out.println("Среднее для fob равно " + x); // Посмотреть, какие массивы имеют одинаковые средние. System.out.print("Средние iob и dob "); if(iob.sameAvg(dob)){ System.out.println("равны."); } else { System.out.println("отличаются."); } System.out.print("Средние iob и fob "); if(iob.sameAvg(fob)){ System.out.println("равны."); } else { System.out.println("отличаются."); } } }

Результат работы этой программы:

Среднее для iob равно 3.0

Среднее для dob равно 3.3

Среднее для fob равно is 3.0

Средние iob и dob отличаются.

Средние iob и fob равны.

И еще один, последний, момент: важно понимать, что шаблон не влияет на то, какого конкретного типа создается объект класса Stats. Этим управляет слово extends в объявлении класса Stats. Шаблон просто соответствует корректному объекту класса Stats.

Ограниченные шаблоны.

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

Рассмотрим следующую иерархию классов, которые инкапсулируют координаты.

// Двухмерные координаты, class TwoD { int х, у; TwoD(int a, int b) { х = а; У = b; } } // Трехмерные координаты, class ThreeD extends TwoD { int z; ThreeD(int a, int b, int c) { super(a, b); z = с ; } } // Четырехмерные координаты, class FourD extends ThreeD { int t; FourD(int a, int b, int c, int d) { super(a, b, c); t = d; } }

На вершине иерархии находится класс TwoD, который инкапсулирует двухмерные координаты XY. Его наследник — класс ThreeD — добавляет третье измерение, описывая координаты XYZ. От класса ThreeD наследуется класс FourD, который добавляет четвертое измерение (время), порождая четырехмерные координаты.

Ниже показан обобщенный класс, называемый Coords, который хранит массив координат:

// Этот класс хранит массив координатных объектов, class Coords<T extends TwoD> { T[] coords; Coords(T[] o) { coords = o; } }

Обратите внимание на то, что класс Coords задает тип параметра, ограниченный классом TwoD. Это значит, что любой массив, сохраненный в объекте класса Coords, будет содержать объект типа TwoD или любой из его подклассов.

Теперь предположим, что вы хотите написать метод, который отображает координаты X и Y для каждого элемента в массиве coords объекта класса Coords.

Поскольку все типы объектов класса Coords имеют, как минимум, пару координат (X и Y), это легко сделать с помощью шаблона:

static void showXY(Coords<?> с) { System.out.println("X Y Coordinates:"); for(int i = 0; i < с.coords.length; i++) { System.out.println(c.coords[i].x + " " + с.coords[i].y); } System.out.println(); }

Поскольку класс Coords — ограниченный обобщенный тип, который задает

класс TwoD как верхнюю границу, все объекты, которые можно использовать для создания объекта класса Coords, будут массивами типа TwoD или классов, наследуемых от него. Таким образом, метод showXY() может отображать содержимое любого объекта класса Coords.

Но что, если вы хотите создать метод, отображающий координаты X, Y и Z объекта классов ThreeD или FourD?

Беда в том, что не все объекты класса Coords будут иметь три координаты, так как тип Coords будет иметь только координаты

X и Y. Как же написать метод, который будет отображать координаты X, Y

и Z для типов Coords и Coords, в то же время предотвращая использование этого метода с объектами класса Coords?

Ответ заключается в использовании ограниченных шаблонов аргументов.

Ограниченный шаблон задает верхнюю или нижнюю границу типа аргумента.

Это позволяет ограничить типы объектов, которыми будет оперировать метод.

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

Применяя ограниченные шаблоны, легко создать метод, отображающий координаты X, Y и Z для объекта класса Coords, если этот объект действительно имеет эти три координаты. Например, следующий метод showXYZ() показывает координаты элементов, сохраненных в объекте класса Coords, если эти элементы имеют тип ThreeD (или унаследованы от класса ThreeD):

static void showXYZ(Coords<? extends ThreeD> c) { System.out.println("X Y Z Coordinates:"); for(int i=0; i < с.coords.length; i++){ System.out.println(c.coords[i].x + " " + с.coords[i].y + " " + с.coords[i].z); } System.out.println(); }

Обратите внимание на то, что слово extends может быть добавлено к шаблону в параметре объявления параметра с. Таким образом, знакоместу ? должен соответствовать любой тип до тех пор, пока он является типом ThreeD или типом, унаследованным от него. То есть ключевое слово extends накладывает верхнее ограничение соответствия на знакоместо ?. Из-за этого ограничения метод showXYZ() может быть вызван со ссылкой на объекты типа Coords или Coords, но не со ссылкой на тип Coords. Попытка вызвать метод showXYZ() со ссылкой на тип Coords вызывает ошибку времени компиляции, что обеспечивает безопасность типов.

Ниже приведена полная программа, которая демонстрирует действие ограниченного шаблона аргумента:

// Ограниченные шаблоны аргумента. // Двухмерные координаты. class TwoD { int х, у; TwoD(int a, int b) { x = a; y = b; } } // Трехмерные координаты, class ThreeD extends TwoD { int z; ThreeD(int a, int b, int c) { super(a, b); } } // Четырехмерные координаты. class FourD extends ThreeD { int t; FourD(int a, int b, int c, int d) { super(a, b, c); t = d; } } // Этот класс хранит массив координатных объектов, class Coords<T extends TwoD> { T[] coords; Coords(T[] o) { coords = o; } } // Демонстрация ограниченных шаблонов, class BoundedWildcard { static void showXY(Coords<?> c) { System.out.println("Координаты X Y:"); for(int i=0; i < с.coords.length; i ++) { System.out.println(c.coords[i].x + " " + с.coords[i].y); } System.out.println(); } static void showXYZ(Coords<? extends ThreeD> c) { System.out.println("Координаты X Y Z:"); for(int i=0; i < с.coords.length; i++){ System.out.println(c.coords[i].x + " " + с.coords[i].у + " " + с.coords[i].z); } System.out.printIn(); } static void showAll(Coords<? extends FourD> c) { System.out.println("Координаты X Y Z T:"); for(int i=0; i < с.coords.length; i++) { System.out.println(c.coords[i].x + " " + с.coords[i].y + " " + с.coords[i].z + " " + с.coords[i].t); } System.out.println(); } public static void main(String args[]) { TwoD td[] = { new TwoD(0, 0), new TwoD(7, 9) , new TwoD(18, 4), new TwoD(-l, -23) }; Coords<TwoD> tdlocs = new Coords<TwoD>(td); System.out.println("Содержимое tdlocs."); showXY(tdlocs); // OK, это TwoD // showXYZ(tdlocs); // Ошибка, не ThreeD // showAll(tdlocs); // Ошибка, не FourD // Теперь создаем несколько объектов FourD. FourD fd[] = { new FourD(1, 2, 3, 4), new FourD(6, 8, 14, 8), new FourD(22, 9, 4, 9), new FourD(3, -2, -23, 17) }; Coords<FourD> fdlocs = new Coords<FourD>(fd); System.out.println("Содержимое fdlocs."); // Здесь все ОК. showXY(fdlocs); showXYZ(fdlocs); showAll(fdlocs) ; } }

Результат работы этой программы выглядит следующим образом:

Содержимое tdlocs.

Координаты X Y:

0 0

7 9

18 4

-1 -23

Содержимое fdlocs.

Координаты X Y:

1 2

6 8

22 9

3 -2

Координаты X Y Z:

12 3

6 8 14

22 9 4

3 -2 -23

Координаты X Y Z Т:

12 3 4

6 8 14 8

22 9 4 9

3 -2 -23 17

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

// showXYZ(tdlocs); // Ошибка, не ThreeD // showAll(tdlocs); // Ошибка, не FourD

Поскольку tdlocs — это объект класса Coords<TwoD>, он не может быть использован для вызова методов showXYZ() или showAll(), потому что ограничивающий шаблон аргумента в их объявлении предотвращает это. Чтобы убедиться в этом, попробуйте убрать комментарии с упомянутых строк и попытаться скомпилировать программу. Вы получите ошибку компиляции по причине несоответствия типов.

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

<? extends суперкласc>

Здесь суперкласс — это имя класса, который служит верхней границей. Помните, что это — включающее выражение, потому что класс, заданный в качестве верхней границы (т.е. суперкласс), также находится в пределах допустимых типов.

Вы также можете указать нижнюю границу шаблона, добавив выражение

super к объявлению шаблона. Вот его общая форма.

<? super подклассы>

В этом случае допустимыми аргументами могут быть только классы, которые являются суперклассами для подклассa. Это исключающая конструкция, поcкольку она не включает класс подклассa.

rss