Java. Полное руководство. 8-е издание. С. Н. Тригуб Перевод с английского и редакция
Скачать 25.04 Mb.
|
Обобщенный класс с двумя параметрами типа Для обобщенного типа можно объявлять более одного параметра типа. Чтобы указать два или более параметров типа, просто используйте разделенный запятыми список. Например, следующий класс TwoGen — это вариант класса Gen, который принимает два параметра Простой обобщенный класс с двумя // параметрами типа Т и V . class TwoGencT, V> Т obi; V оЬ2 ; // Передать конструктору ссылки на объект типа Т и объект типа V. T w o G e n То ; оЬ2 = о 2 ; } // Показать типы Т и V. void showTypes() Тип T: " + o b i Тип V: " + o b 2 .getClass().getName()); } T ge t o b l () { return o b i ; } V g e tob2() { return o b 2 ; } Глава 14. Обобщения 5 9 // Демонстрация TwoGen. class SimpGen { public static void main(String a r g s []) { TwoGencInteger, String> tgObj = new TwoGen String>(88, "Обобщения Показать типы tgObj.showTypes(); // Получить и показать значения int v = tgObj.g e Значение " + v ) ; String str = tgObj.ge t o b 2 (Значение " + Результат работы этой программы. Тип Т java.lang.Integer Тип V: java.lang.String Значение 88 Значение Обобщения Обратите внимание на объявление класса TwoGen. class TwoGen V> Оно задает два параметра типа — Т и V, — разделенные запятой. Поскольку оно имеет два параметра типа, при создании объекта класса TwoGen должны быть переданы два аргумента типа, как показано ниже String> tgObj = new TwoGencInteger, String> (88, "Обобщения"); В этом случае класс Integer подставляется вместо параметра Та класс String — вместо параметра Хотя два аргумента в примере отличаются, допустимо передать в параметрах два одинаковых типа. Например, следующая строка кода вполне корректна, String> х = new TwoGen String> ("A", "В"); В этом случае оба аргумента параметров типа V и Т будут иметь тип String. Конечно, если оба аргумента всегда будут одинаковы, то два параметра не обяза тельны. Общая форма обобщенного класса Синтаксис, показанный в предыдущих примерах, может быть обобщен. Так выглядит синтаксис объявления обобщенного класса имя_класса<список_параметров_типа> { // Ниже показан синтаксис объявления ссылки на обобщенный класс. имя_класса<список_аргументов_типа> имя_переменной = new имя_класса<список_аргументов_типа> ( список_аргум ентов_констант) ; 3 6 Часть I. Язык Ограниченные типы В предыдущих примерах параметры типов могли быть заменены любыми типами классов. Это подходит ко многим случаям, но иногда удобно ограничить перечень типов, передаваемых в параметрах. Предположим, что вы хотите создать обобщенный класс, который содержит метод, возвращающий среднее значение массива чисел. Более того, вы хотите использовать этот класс для получения среднего значения чисел, включая целые числа и числа с плавающей точкой одинарной и двойной точности. То есть вы хотите указать тип числовых данных обобщенно, используя параметр типа. Чтобы создать такой класс, можно попробовать что-то вроде этого Stats пытается (безуспешно) создать // обобщенный класс, который вычисляет // среднее значение массива чисел // заданного типа / // Класс содержит ошибку class Stats } // Возвращает double во всех случаях average() { double sum = 0.0; for(int i=0; i < nums.length; i++) sum + = n u m s [i ].doubleValue(); // Ошибка sum / Метод average () класса Stats пытается получить версию типа double каждого числа в массиве nums, вызывая метод doubleValue () . Поскольку все числовые классы, такие как Integer и Double, являются подклассами Number, а класс Number определяет метод doubleValue (), этот метод доступен всем числовым классам-оболочкам. Проблема в том, что компилятор не имеет возможности узнать, что вы намерены создавать объекты класса Stats, используя только числовые типы. То есть, когда вы компилируете класс Stats, выдается сообщение об ошибке, свидетельствующее о том, что метод doubleValue () неизвестен. Чтобы решить эту проблему, вам нужен какой-то способ сообщить компилятору, что вы собираетесь передавать в параметре Т только числовые типы. Более того, необходим еще некоторый способ гарантии того, что будут передаваться только числовые типы. Чтобы справиться с этой ситуацией, язык Java предлагает ограниченные типы Когда указывается параметр типа, вы можете создать ограничение сверху, которое объявляет суперкласс, от которого должны быть унаследованы все аргументы типов. Для этого используется ключевое слово extends при указании параметра типа, как показано ниже. <Т extends супер кл ас о Это означает, что параметр Т может быть заменен только классом суперкласс либо его подклассами. То есть суперкласс объявляет включающую верхнюю границу. Вы можете использовать ограничение сверху, чтобы исправить класс S t a t s , Глава 14. Обобщения 3 6 показанный выше, указав класс Number как верхнюю границу используемого параметра типа В этой версии Stats аргумент типа '/ Т должен быть либо Number, либо классом / унаследованным от него массив Number или подклассов Передать конструктору ссылку на массив // элементов Number или его подклассов о) { 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 a r g s []) { Integer inums[] = { 1, 2, 3, 4, 5 }; Stats double v = i o b Среднее значение iob равно " + v ) ; Double d n u m s [] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats double w = d o b Среднее значение dob равно " + w ) ; // Это не скомпилируется, потому что String не является // подклассом Number. // String strs[] = { "1", "2", "3", "4", "5" }; // Stats // double x = strob.average(); // Среднее значение strob равно " + v ) Результат работы этой программы выглядит следующим образом. Среднее значение iob равно 3.0 Среднее значение dob равно Обратите внимание на то, что класс S t a t s теперь объявлен так StatscT extends Number> Поскольку тип Т теперь ограничен классом Number, компилятор Java знает, что все объекты типа Т могут вызывать метод doubleValue () , так как это метод класса Number. Это уже серьезное преимущество. Однако в качестве дополнительного бонуса ограничение параметра Т также предотвращает создание нечисловых объектов класса Stats. Например, если вы попытаетесь убрать комментарии в строках 3 6 Часть I. Язык находящихся в конце программы, и перекомпилировать ее, то получите ошибку времени компиляции, потому что класс String не является подклассом В дополнение к использованию типа класса как ограничения, вы можете также применять тип интерфейса. Фактически вы можете указывать в качестве ограничений множество интерфейсов. Более того, такое ограничение может включать как тип класса, таки один или более интерфейсов. В этом случае тип класса должен быть задан первым. Когда ограничение включает тип интерфейса, допустимы только аргументы типа, реализующие этот интерфейс. Указывая ограничение, имеющее класс и интерфейс либо множество интерфейсов, применяйте оператор & для их объединения GencT extends MyClass & Mylnterface> { // Здесь параметр T ограничен классом по имении интерфейсом Mylnteface. То есть любой тип, переданный параметру Т, должен быть подклассом класса MyClass и иметь реализацию интерфейса Использование шаблонов аргументов Кроме того, что контроль типов удобен сам по себе, иногда он позволяет получить чрезвычайно полезные конструкции. Например, класс S t a t s , рассмотренный в предыдущем разделе, предполагает, что вы хотите добавить метод по имени sameAvg ( ), который определяет, содержат ли два объекта класса S t a t s массивы, порождающие одинаковое среднее значение, независимо оттого, какого типа числовые значения в них содержатся. Например, если один объект содержит значения типа d o u b le 1.0, 2.0 и 3.0, а другой — целочисленные значения 2, 1 и 3, то среднее значение у них будет одинаково. Один из способов реализации метода sameAvg () — передать ему аргумент класса S t a t s , а затем сравнивать его среднее значение со средним значением вызывающего объекта, возвращая значение t r u e , если они равны. Например, необходимо иметь возможность вызывать метод sameAvg ( ), как показано ниже inums[] = { 1, 2 , 3 , 4, 5 }; Double d n u m s [] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats Stats i f (Средние значения одинаковы."); else System.out.println("Средние значения отличаются."); Вначале написание метода sameAvg () кажется простой задачей. Поскольку класс Stats является обобщенными его метод average () может работать с объектами класса Stats любого типа, кажется, что написание метода sameAvg () не представляет сложности. К сожалению, проблема появляется сразу, как только вы попытаетесь объявить параметр типа S t a t s . Поскольку S t a t s — параметризованный тип, какой тип параметра вы укажете для Stats, когда создадите параметр типа Сначала вы можете подумать о решении вроде такого, в котором параметр Т будет использоваться как параметр типа Это не сработает Определение эквивалентности двух средних значений Глава 14. Обобщения 3 6 3 jcoolean sameAvg(Stats if(average() == o b .average()) return true; return С приведенным выше кодом связана проблема, которая заключается в том, что такой код будет работать только с другим объектом класса Stats, тип которого совпадает с вызывающим объектом. Например, если вызывающий объект имеет тип S t a t s < I n t e g e r > , то параметр o b также должен иметь тип S t a t s < I n t e g e r > . Он, например, не может применяться для сравнения среднего значения типа 3 t a t s < D o u b l e > со средним значением типа S t a t s < S h o r t > . Таким образом, этот подход будет работать только в очень ограниченном контексте и не представляет собой общего (а стало быть, и обобщенного) решения. Чтобы создать обобщенную версию метода s a m e A v g (), следует использовать другое средство обобщений Java — шаблоны аргументов. Шаблон аргумента указывается символом ? и представляет собой неизвестный тип. Применение шаблона — единственный способ написать работающий метод s a m e A v g (). / Определение эквивалентности двух средних значений Отметим использование шаблонного символа ooolean sameAvg(Stats> ob) { if(average() == o b .average()) return true; return Здесь часть S t a t s < ? > соответствует любому объекту класса Stats, что позволяет сравнивать между собой средние значения любых двух объектов класса Stats. Это демонстрируется в следующей программе Использование шаблона class StatscT extends Number> { T [] nums; // массив Number или подклассов Передать конструктору ссылку на массив // типа Number или его подклассов о) { nums = о ; } // Во всех случаях возвращает double, double average() { double sum = 0.0; for(int i=0; i < nums.length; i++) sum + = n u m s [i ].doubleValue(); return sum / nums.length; } // Определение эквивалентности двух средних Обратите внимание на использование шаблонов boolean sameAvg(Stats> ob) { if(average() == o b .average()) return true; return false; 3 6 4 Часть I. Язык Java } } // Демонстрация шаблона WildcardDemo { public static void main(String a r g s []) { Integer inums[] = { 1, 2 , 3 , 4, 5 }; Stats double v = Среднее для iob равно " + v ) ; Double dn u m s [] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; Stats double w = d o b Среднее для dob равно " + w ) ; Float fnums[] = { 1.0F, 2.OF, 3.0F, 4.OF, 5.OF }; Stats double x = Среднее для fob равно " + x ) ; // Посмотреть, какие массивы имеют одинаковые средние. System.out.print("Средние iob и dob "); i f (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 равно О Среднее для dob равно 3.3 Среднее для fob равно is 3.0 Средние iob и dob отличаются. Средние iob и fob равны. И еще один, последний, момент важно понимать, что шаблон не влияет на то, какого конкретного типа создается объект класса S t a t s . Этим управляет слово e x te n d s в объявлении класса S t a t s . Шаблон просто соответствует корректному объекту класса S t a t s Ограниченные шаблоны Шаблоны аргументов могут быть ограничены почти таким же способом, как параметры типов. Ограниченный шаблон особенно важен, когда вы создаете обобщенный тип, оперирующий иерархией классов. Чтобы понять, почему, обратимся к примеру. Рассмотрим следующую иерархию классов, которые инкапсулируют координаты Двухмерные координаты class TwoD { int х , у ; TwoD(int a, int b) { Глава 14. Обобщениях а; У = Ь Трехмерные координаты 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 = На вершине иерархии находится класс TwoD, который инкапсулирует двухмерные координаты XY. Его наследник — класс ThreeD — добавляет третье измерение, описывая координаты XYZ. От класса ThreeD наследуется класс FourD, который добавляет четвертое измерение (время, порождая четырехмерные координаты. Ниже показан обобщенный класс, называемый Coords, который хранит массив координат Этот класс хранит массив координатных объектов class CoordscT extends TwoD> { T [] coords; Co o r d s (T [] o) { coords = o; Обратите внимание на то, что класс Coords задает тип параметра, ограниченный классом TwoD. Это значит, что любой массив, сохраненный в объекте класса Coords, будет содержать объект типа TwoD или любой из его подклассов. Теперь предположим, что вы хотите написать метод, который отображает координаты X и Y для каждого элемента в массиве coords объекта класса Coords. Поскольку все типы объектов класса Coords имеют, как минимум, пару координат (X и Y), это легко сделать с помощью шаблона void showXY(Coords> с) { System.out.p r intln("X Y Coordinates:"); for(int i=0; i < с .coords.length; i++) System.out.println(c.coords[i].x + " " + с .coords[i ] Поскольку класс Coords — ограниченный обобщенный тип, который задает класс TwoD как верхнюю границу, все объекты, которые можно использовать для создания объекта класса Coords, будут массивами типа TwoD или классов, наследуемых от него. Таким образом, метод showXY () может отображать содержимое любого объекта класса Coords. } 3 6 6 Часть I. Язык Но что если вы хотите создать метод, отображающий координаты X, Y и Z объекта классов ThreeD или FourD? Беда в том, что не все объекты класса Coords будут иметь три координаты, так как тип Coords X и Y. Как же написать метод, который будет отображать координаты X, Y и Z для типов 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 + " " + ThreeD или типом, унаследованным от него. То есть ключевое слово extends накладывает верхнее ограничение соответствия на знакоместо ?. Из-за этого ограничения метод showXYZ () может быть вызван со ссылкой на объекты типа Coords Coords Coords () со ссылкой на тип Coords Ниже приведена полная программа, которая демонстрирует действие ограниченного шаблона аргумента Ограниченные шаблоны аргумента Двухмерные координаты TwoD { |