Текст книги "Java: руководство для начинающих (ЛП)"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 28 (всего у книги 36 страниц)
NumericFns dOb = new NumericFns(1.25) ; NumericFns fOb = new NumericFns(-1.25) ;
if(dOb.absEqual(fOb)) System.out.println("Absolute values are the same."); else System.out.println("Absolute values differ."); На первый взгляд может показаться, что при выполнении метода absEqual () не должно возникнуть никаких затруднений, но это совсем не так. Затруднения начнутся при первой же попытке объявить параметр типа NumericFns. Каким он должен быть? Казалось бы, подходящим должно быть следующее решение, где т указывается в качестве параметра типа:
//Не пройдет!
// определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns ob) { if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; } В данном случае для определения абсолютного значения каждого числа используется стандартный метод Math. abs (). Полученные значения сравниваются. Но дело в том, что рассматриваемое здесь решение окажется пригодным лишь в том случае, если объект класса NumericFns, передаваемый в качестве параметра, имеет тот же тип, что и текущий объект. Так, если текущий объект относится к типу NumericFns
// определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns> ob) { // обратите внимание на метасимвол if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue()) return true; return false; } В данном случае выражение NumericFns> соответствует любому типу объекта из класса NumericFns и позволяет сравнивать абсолютные значения в двух произвольных объектах класса NumericFns. Ниже приведен пример программы, демонстрирующий применение метасимвольного аргумента.
// Применение метасимвольного аргумента, class NumericFns { T num; // передать конструктору ссылку на числовой объект NumericFns(Т п) { num = п; } // возвратить обратную величину double reciprocal() { return 1 / num.doubleValue(); } // возвратить дробную часть double fraction() { return num.doubleValue() – num.intValue(); } // определить равенство абсолютных значений в двух объектах boolean absEqual(NumericFns> ob) { if(Math.abs(num.doubleValue()) == Math.abs(ob.num.doubleValue())) return true; return false; } // ...
} // продемонстрировать применение метасимвольного аргумента class WildcardDemo { public static void main(String args[]) { NumericFns iOb = new NumericFns(6) ; NumericFns dOb = new NumericFns(-6.0) ; NumericFns 10b = new NumericFns(5L); System.out.println("Testing iOb and dOb."); // В этом вызове метода тип метасимвольного // аргумента совпадает с типом Double. if(iOb.absEqual(dOb)) System.out.println("Absolute values are equal."); else System.out.println("Absolute values differ."); System.out.println(); System.out.println("Testing iOb and 10b."); // А в этом вызове метода тип метасимвольного // аргумента совпадает с типом Long. if(iOb.absEqual(10b)) System.out.println("Absolute values are equal."); else System.out.println("Absolute values differ."); }
} Выполнение этой программы дает следующий результат:
Testing iOb and dOb. Absolute values are equal.
Testing iOb and 10b. Absolute values differ. Обратите внимание на два следующих вызова метода absEqual ():
if(iOb.absEqual(dOb))
if(iOb.absEqual(10b)) В первом вызове переменная iOb указывает на объект типа NumericFns
class А { // ... }
class В extends А { // ... }
class С extends А { // ... }
// Обратите внимание на то, что D не является подклассом А. class D { // ... } Здесь класс А является суперклассом для классов В и С, но не для класса D. Теперь рассмотрим очень простой обобщенный класс.
// Простой обобщенный класс. class Gen { ^ Т ob; Gen(Т о) { ob = о; }
} В классе Gen предусмотрен один параметр типа, который определяет тип объекта, хранящегося в переменной ob. Как видите, на тип Т не накладывается никаких ограничения. Следовательно, параметр типа Т может обозначать любой класс. А теперь допустим, что требуется создать метод, принимающий аргумент любого типа, соответствующего объекту класса Gen, при условии, что в качестве параметра типа этого объекта указывается класс А или его подклассы. Иными словами, требуется создать метод, который оперирует только объектами типа Gen<тип>, где тип – это класс А или его подклассы. Для этой цели нужно воспользоваться ограниченным метасимволь– ным аргументом. Ниже для примера приведено объявление метода test (), которому в качестве аргумента может быть передан только объект класса Gen, на параметр типа которого накладываются следующие ограничения: соответствие классу А или его подклассам.
// Здесь знак ? устанавливает соответствие // классу А или производным от него подклассам, static void test(Gen extends A> o) { // ... } А приведенный ниже пример класса демонстрирует типы объектов класса Gen, которые могут быть переданы методу test ().
class UseBoundedWildcard { // Здесь знак ? устанавливает соответствие // классу А или производным от него подклассам. //В объявлении этого метода используется ограниченный // метасимвольный аргумент. static void test(Gen extends A> о) { // ... } public static void main(String args[]) { A a = new A(); В b = new В() ; С с = new C(); D d = new D() ; Gen w = new Gen(a); Gen w2 = new Gen(b); Gen
} В методе main () создаются объекты классов А, В, С и D. Затем они используются для создания четырех объектов класса Gen (по одному на каждый тип). После этого метод test () вызывается четыре раза, причем последний его вызов закомментирован. Первые три вызова вполне допустимы, поскольку w, w2 и w3 являются объектами класса Gen, типы которых определяются^ классом А или производными от него классами. А последний вызов метода test () недопустим, потому что w4 – это объект класса D, не являющегося производным от к класса А. Следовательно, ограниченный метасимвольный аргумент в методе test () не позволяет передавать ему объект w4 в качестве параметра. В целом верхняя граница для метасимвольного аргумента задается в следующей общей форме:
extends суперкласс > где после ключевого слова extends указывается суперкласс, т.е. имя класса, определяющего верхнюю границу, включая и его самого. Это означает, что в качестве аргумента допускается указывать не только подклассы данного класса, но и сам этот класс. По мере необходимости можно также указать нижнюю границу для метасимвольного аргумента. Для этой цели служит ключевое слово super, указываемое в следующей общей форме:
extends подкласс > В данном случае в качестве аргумента допускается использовать только суперклассы, от которых наследует подкласс, исключая его самого. Это означает, что подкласс, определяющий нижнюю границу, не относится к числу классов, передаваемых в качестве аргумента. В этом случае следующее приведение типов может быть выполнено, поскольку переменная х указывает на экземпляр класса Gen
(Gen) х // Допустимо А следующее приведение типов не может быть выполнено, поскольку переменная х не указывает на экземпляр класса Gen
(Gen) х // Недопустимо ## Обобщенные методы Как было показано в предыдущих примерах, методы в обобщенных классах могут быть объявлены с параметром типа своего класса, а следовательно, такие методы автоматически становятся обобщенными относительно параметра типа. Но можно также объявить обобщенный метод с одним или несколькими параметрами его собственного типа. Более того, такой метод может быть объявлен в обычном, а не обобщенном классе. Ниже приведен пример программы, в которой объявляется класс GenericMethodDemo, не являющийся обобщенным. В этом классе объявляется статический обобщенный метод arraysEqualO, в котором определяется, содержатся ли в двух массивах одинаковые элементы, расположенные в том ж самом порядке. Такой метод можно использовать для сравнения двух массивов одинаковых или совместимых между собой типов.
// Пример простого обобщенного метода, class GenericMethodDemo { // Этот обобщенный метод определяет, // совпадает ли содержимое двух массивов. static <Т, V extends Т> boolean arraysEqual(Т[] х, V[] у) { // Если массивы имеют разную длину, они не могут быть одинаковыми, if(х.length != у.length) return false; for(int i=0; i < x.length; i++) if(!x[i].equals(y[i])) return false; // Массивы отличаются. return true; // Содержимое массивов совпадает. } public static void main(String args[]) { Integer nums[] = { 1, 2, 3, 4, 5 }; Integer nums2[] = {1, 2, 3, 4, 5 }; Integer nums3[] = {1, 2, 7, 4, 5 }; Integer nums4[] = {1, 2, 7, 4, 5, 6}; // Аргументы типа T и V неявно определяются при вызове метода. if(arraysEqual(nums, nums)) System.out.println("nums equals nums"); if(arraysEqual(nums, nums2)) System.out.println("nums equals nums2"); if(arraysEqual(nums, nums3)) System.out.println("nums equals nums3"); if(arraysEqual(nums, nums4)) System.out.println("nums equals nums4"); // создать массив объектов типа Double Double dvals[] = { 1.1, 2.2, 3.3, 4.4, 5.5 }; // Следующая строка не будет скомпилирована, так как // типы массивов nums и dvals не совпадают. // if(arraysEqual(nums, dvals)) // System.out.println("nums equals dvals"); }
} Результат выполнения данной программы выглядит следующим образом:
nums equals nums nums equals nums2 Рассмотрим подробнее исходный код метода arraysEqual (). Посмотрите прежде всего, как он объявляется:
static <Т, V extends Т> boolean arraysEqual(Т[] х, V[] у) { Параметры типа указываются перед возвращаемым типом. Обратите далее внимание на то, что верхней границей для типа параметра V является тип параметра Т. Таким образом, тип параметра V должен быть таким же, как и у параметра Т, или же быть его подклассом. Такая связь гарантирует, что при вызове метода arraysEqual () могут быть указаны только совместимые друг с другом параметры. И наконец, обратите внимание на то обстоятельство, что метод arraysEqual () объявлен как static, т.е. его можно вызывать независимо от любого объекта. Но обобщенные методы не обязательно должны быть статическими. В этом смысле на них не накладывается никаких ограничений. А теперь проанализируем, каким образом метод arraysEqual () вызывается в методе main (). Для этого используется обычный синтаксис, а параметры типа не указываются. И это становится возможным потому, что типы аргументов данного метода распознаются автоматически, а типы параметров Т и V настраиваются соответствующим образом. Рассмотрим в качестве примера первый вызов метода arraysEqual ():
if(arraysEqual(nums, nums)) В данном случае типом первого аргумента является Integer, который и заменяет тип параметра Т. Таким же является и тип второго аргумента, а следовательно, тип параметра V также заменяется на Integer. Следовательно, выражение для вызова метода arraysEqual () составлено правильно, и оба массива можно сравнить друг с другом. Обратите далее внимание на следующие закомментированные строки:
// if(arraysEqual(nums, dvals)) // System.out.println("nums equals dvals"); Если удалить в них символы комментариев и попытаться скомпилировать программу, то компилятор выдаст сообщение об ошибке. Дело в том, что верхней границей для типа параметра V является тип параметра Т. Этот тип указывается после ключевого ело– ва extends, т.е. тип параметра V может быть таким же, как и у параметра т, или быть его подклассом. В данном случае типом первого аргумента рассматриваемого здесь метода является Integer, заменяющий тип параметра т, тогда как типом второго аргумента – Double, не являющийся подклассом Integer. Таким образом, вызов метода arraysEqual () оказывается недопустимым, что и приводит к ошибке при компиляции. Синтаксис объявления метода arraysEqual () может быть обобщен. Ниже приведена общая форма объявления обобщенного метода.
<параметрытипа> возвращаемыйтип имя_метода (параметры) { // ... Как и при вызове обычного метода, параметры_типа разделяются запятыми. В обобщенном методе их список предваряет возвращаемый_тип. ## Обобщенные конструкторы Конструктор может быть обобщенным, даже если сам класс не является таковым. Например, в приведенной ниже программе класс Summation не является обобщенным, но в нем используется обобщенный конструктор.
// Применение обобщенного конструктора, class Summation { private int sum; // Обобщенный конструктор.
}
class GenConsDemo { public static void main(String args[]) { Summation ob = new Summation(4.0); System.out.println("Summation of 4.0 is " + ob.getSum()); }
} В классе Summation вычисляется и инкапсулируется сумма всех чисел от 0 до N, причем значение N передается конструктору. Для конструктора Summation () указан параметр типа, ограниченный сверху классом Number, и поэтому объект типа Summation может быть создан с использованием любого числового типа, в том числе Integer, Float и Double. Независимо от используемого числового типа, соответствующее значение преобразуется в тип Integer при вызове intValue (), а затем вычисляется требуемая сумма. Таким образом, класс Summation совсем не обязательно объявлять обобщенным – достаточно сделать обобщенным только его конструктор. ## Обобщенные интерфейсы Наряду с обобщенными классами и методами существуют также обобщенные интерфейсы. Такие интерфейсы определяются подобно обобщенным классам. Их применение демонстрируется в приведенном ниже примере программы. В ней создается интерфейс Containment, который может быть реализован классами, хранящими одно или несколько значений. Кроме того, в этой программе объявляется метод contains (), в котором определяется, содержится ли указанное значение в текущем объекте.
// Пример обобщенного интерфейса.
// В этом интерфейсе подразумевается, что реализующий // его класс содержит одно или несколько значений, interface Containment { // обобщенный интерфейс // Метод contains() проверяет, содержится ли // некоторый элемент в объекте класса, // реализующего интерфейс Containment, boolean contains(Т о); }
// реализовать интерфейс Containment с помощью массива, // предназначенного для хранения значений. // Любой класс, реализующий обобщенный интерфейс, // также должен быть обобщенным. class MyClass implements Containment { T[] arrayRef; MyClass(T[] o) { arrayRef = o; } // реализовать метод contains() public boolean contains(T o) { for(T x : arrayRef) if(x.equals(o)) return true; return false; }
}
class GenlFDemo { public static void main(String args[]) { Integer x[] = { 1, 2, 3 }; MyClass
} Выполнение этой программы дает следующий результат:
2 is in ob 5 is NOT in ob Большую часть исходного кода этой программы нетрудно понять, но на некоторых ее особенностях следует все же остановиться. Обратите прежде всего внимание на то, как объявляется интерфейс Containment:
interface Containment { Обобщенные интерфейсы объявляются таким же образом, как и обобщенные классы. В данном случае параметр типа Т задает тип включаемого объекта. Интерфейс Containment реализуется классом MyClass. Объявление этого класса выглядит следующим образом:
class MyClass implements Containment { Если класс реализует обобщенный интерфейс, то он также должен быть обобщенным. В нем должен быть объявлен как минимум тот же параметр типа, который указан в объявлении интерфейса. Например, приведенный ниже вариант объявления класса MyClass недопустим.
class MyClass implements Containment { // Ошибка! В данном случае ошибка заключается в том, что в классе MyClass не объявлен параметр типа, а это означает, что передать параметр типа интерфейсу Containment нельзя. Если идентификатор Т останется неизвестным, компилятор выдаст сообщение об ошибке. Класс, реализующий обобщенный интерфейс, может не быть обобщенным только в одном случае: если при объявлении класса для интерфейса указывается конкретный тип. Такой способ объявления класса приведен ниже,
class MyClass implements Containment { // Допустимо Вас теперь вряд ли удивит, что один или несколько параметров типа для универсального интерфейса могут быть ограничены. Это позволяет указывать, какие именно типы данных допустимы для интерфейса. Так, если требуется запретить передачу интерфейсу Containment значений, не являющихся числовыми, для этой цели интерфейс можно объявить следующим образом:
interface Containment { Теперь любой класс, реализующий интерфейс Containment, должен передавать ему значение типа, удовлетворяющее указанным выше ограничениям. Например, класс MyClass, реализующий данный интерфейс, должен объявляться следующим образом:
class MyClass implements Containment { Обратите особое внимание на то, как параметр типа Т объявляется в классе MyClass, а затем передается интерфейсу Containment. На этот раз интерфейсу Containment требуется тип, расширяющий тип Number, поэтому в классе MyClass, реализующем этот интерфейс, должны быть указаны соответствующие ограничения. Если верхняя граница задана в объявлении класса, то ее нет необходимости указывать еще раз в операторе implements. Если же попытаться сделать это, будет получено сообщение об ошибке. Например, следующее выражение составлено неверно и не будет скомпилировано:
// Ошибка! class MyClass implements Containment { Если параметр типа задан в объявлении класса, он лишь передается интерфейсу без дальнейших видоизменений. Ниже приведена общая форма объявления обобщенного интерфейса.
interface имяинтерфейса<параметрытипа> { // ... где параметры_типа указываются списком через запятую. При реализации обобщенного интерфейса в объявлении класса также должны быть указаны параметры типа. Общая форма объявления класса, реализующего обобщенный интерфейс, приведена ниже.
class имякласса<параметрытипа> implements имяинтерфейса<параметрытипа> { **Пример для опробования 13.1.** Создание обобщенного класса очереди Главным преимуществом обобщенных классов является возможность создания надежного кода, пригодного для повторного использования. Как пояснялось в начале главы, многие алгоритмы могут быть реализованы одинаково независимо от типа данных. Например, очередь в равной степени пригодна для хранения целых чисел, символьных строк, объектов типа File и других типов данных. Вместо того чтобы создавать отдельный класс очереди для объектов каждого типа, можно разработать единое обобщенное решение, пригодное для обращения с объектами любого типа. В итоге цикл проектирования, программирования, тестирования и отладки кода будет выполняться только один раз, не повторяясь всякий раз, когда потребуется организовать очередь для нового типа данных. В этом проекте предстоит в очередной и последний раз видоизменить класс очереди, разработка которого была впервые начата в главе 5. Для этой цели будет объявлен обобщенный интерфейс, определяющий операции над очередью, созданы два класса исключений и реализована очередь фиксированного размера. Разумеется, вам ничто не помешает поэкспериментировать с другими разновидностями обобщенных очередей, например, создать динамическую или циклическую очередь, следуя приведенным ниже рекомендациям. Кроме того, исходный код, реализующий очередь в этом проекте, будет организован в виде ряда отдельных файлов. С этой целью код интерфейса, исключений, реализации очереди фиксированного размера и программы, демонстрирующей очередь в действии, будет распределен по отдельным исходным файлам. Такая организация исходного кода отвечает подходу, принятому в работе над большинством реальных проектов. Последовательность действий 1. Первым этапом создания обобщенной очереди станет формирование обобщенного интерфейса, описывающего две операции над очередью: размещение и извлечение. Обобщенная версия интерфейса очереди будет называться iGenQ, ее исходный код приведен ниже. Введите этот код во вновь созданный файл IGenQ. java. // Обобщенный интерфейс очереди, public interface IGenQ
Далее создайте файл QExc. j ava. Введите в него два приведенных ниже класса, в которых определяются исключения, возникающие в работе с очередью. // Исключение в связи с ошибками переполнения очереди, class QueueFullException extends Exception { int size; QueueFullException(int s) { size = s; } public String toString() { return "nQueue is full. Maximum size is " + size; } } // Исключение в связи с ошибками опустошения очереди, class QueueEmptyException extends Exception { public String toString() { return "nQueue is empty."; } }
В этих классах определяются две ошибки, которые могут возникнуть в работе с очередью: попытки поместить элемент в заполненную очередь и извлечь элемент из пустой очереди. Эти классы не являются обобщенными, поскольку они действуют одинаково, независимо от типа данных, хранящихся в очереди.
Создайте файл GenQueue.java. Введите в него приведенный ниже код, в котором реализуется очередь фиксированного размера. // Обобщенный класс, реализующий очередь фиксированного размера, class GenQueue
Класс GenQueue объявляется как обобщенный с параметром типа Т. Этот параметр определяет тип данных, хранящихся в очереди. Обратите внимание на то, что параметр типа Т также передается интерфейсу iGenQ.
Конструктору GenQueue передается ссылка на массив, используемый для хранения элементов очереди. Следовательно, для построения объекта класса GenQueue нужно сначала сформировать массив, тип которого будет совместим с типом объектов, сохраняемых в очереди, а его размер достаточен для размещения этих объектов в очереди. В рассматриваемом здесь коде первый элемент массива не используется, поэтому длина массива должна быть на один элемент больше, чем количество элементов, которые допускается хранить в очереди. Например, в следующих строках кода демонстрируется создание очереди для хранения символьных строк: String strArray[] = new String[10]; GenQueue
Создайте файл GenQDemo.java и введите в него приведенный ниже код. В этом коде демонстрируется работа обобщенной очереди. /* Проект 13.1. Демонстрация обобщенного класса очереди. */ class GenQDemo { public static void main(String args[]) { // создать очередь для хранения целых чисел Integer iStoref] = new Integer[10]; GenQueue
Скомпилируйте программу и запустите ее на выполнение. В итоге на экране отобразится следующий результат: ``` Demonstrate a queue of Integers. Adding 0 to the q. Adding 1 to the q. Adding 2 to the q. Adding 3 to the q. Adding 4 to the q.
Getting next Integer from q: 0 Getting next Integer from q: 1 Getting next Integer from q: 2 Getting next Integer from q: 3 Getting next Integer from q: 4
Demonstrate a queue of Doubles. Adding 0.0 to the q2. Adding 0.5 to the q2. Adding 1.0 to the q2. Adding 1.5 to the q2. Adding 2.0 to the q2.
Getting next Double from q2: 0.0 Getting next Double from q2: 0.5 Getting next Double from q2: 1.0 Getting next Double from q2: 1.5 Getting next Double from q2: 2.0 Попытайтесь самостоятельно сделать обобщенными классы CircularQueue и DynQueue, созданные в примере для опробования 8.1. ## Базовые типы и устаревший код Обобщенные типы появились лишь в версии JDK 5, поэтому в Java нужно было принять меры для обеспечения совместимости с созданным ранее и впоследствии устаревшим кодом. Короче говоря, устаревший код нужно было оставить вполне работоспособным и совместимым с обобщениями. А это означало, что устаревший код должен был нормально взаимодействовать с обобщениями, и наоборот. Для обеспечения перехода к обобщенным типам в Java предусмотрена возможность создания обобщенных классов, используемых без указания аргументов типа. Это подразумевает создание для класса базового типа, иногда еще называемого сырым. Такой тип совместим с устаревшим кодом, в котором ничего не известно об обобщенных классах. Единственный недостаток базового типа заключается в том, что при его использовании теряются преимущества типовой безопасности, присущие обобщениям. Ниже приведен пример программы, демонстрирующий применение базового типа.
// Применение базового типа, class Gen { Т ob; // объявить объект типа Т // передать конструктору ссылку на объект типа Т Gen(Т о) { ob = о; } // возвратить объект ob Т getob() { return ob; }
}
// продемонстрировать использование базового типа class RawDemo { public static void main(String args[]) { // создать объект класса Gen для типа Integer Gen
} У этой программы имеется ряд интересных особенностей. Прежде всего, базовый тип обобщенного класса Gen создается в следующем объявлении:
Gen raw = new Gen(new Double(98.6)); В данном случае аргументы типа не указываются. В итоге создается объект класса Gen, тип Т которого замещается типом Object. Базовые типы не обеспечивают типовую безопасность. Переменной базового типа может быть присвоена ссылка на любой тип объекта класса Gen. Справедливо и обратное: переменной конкретного типа из класса Gen может быть присвоена ссылка на объект класса Gen базового типа. Обе операции потенциально опасны, поскольку они действуют в обход механизма проверки типов, обязательной для обобщений. Недостаточный уровень типовой безопасности демонстрируют примеры в закомментированных строках кода в конце данной программы. Рассмотрим их по отдельности. Сначала проанализируем следующую строку кода: