Skip to content

Latest commit

 

History

History
1332 lines (1134 loc) · 123 KB

c13.md

File metadata and controls

1332 lines (1134 loc) · 123 KB

Глава 13

Обобщения

Основные навыки и понятия

  • Преимущества обобщений
  • Создание обобщенного класса
  • Применение ограниченных параметров типов
  • Использование метасимвольных аргументов
  • Применение ограниченных метасимвольных аргументов
  • Создание обобщенного метода
  • Создание обобщенного конструктора
  • Создание обобщенного интерфейса
  • Использование базовых типов
  • Выводимость типов с помощью ромбовидного оператора
  • Стирание
  • Исключение ошибок неоднозначности
  • Наложение ограничений на обобщения

Начиная с версии 1.0 в Java было реализовано много новых языковых средств. Все они очень полезны и расширяют область применения Java, но на одном из них следует остановиться особо, поскольку оно оказывает огромное влияние на язык в целом. Речь идет об обобщениях — совершенно новой синтаксической конструкции, появление которой вызвало существенные изменения во многих классах и методах базового интерфейса API. Не будет преувеличением сказать, что введение обобщений коренным образом изменило сам язык Java.

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

Основные положения об обобщениях

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

Главное преимущество обобщенного кода состоит в том, что он автоматически настраивается на работу с нужным типом данных. Многие алгоритмы выполняются одинаково, независимо от того, к данным какого типа они должны применяться. Например, быстрая сортировка не зависит от типа данных, в качестве которого можно использовать Integer, String, Object и даже Thread. Используя обобщения, можно реализовать алгоритм один раз, а затем применять его без особого труда к любому типу данных.

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

Но дело в том, что в таком коде трудно было соблюсти типовую безопасность, поскольку для преобразования типа Object в конкретный тип данных требовалось приведение типов. А это служило потенциальным источником ошибок из-за того, что приведение типов могло быть неумышленно выполнено неверно. Это затруднение позволяют преодолеть обобщения, обеспечивая типовую безопасность, которой раньше так недоставало. Кроме того, обобщения упрощают весь процесс, поскольку исключают необходимость выполнять приведение типов для преобразования объекта или другого типа обрабатываемых данных. Таким образом, обобщения расширяют возможности повторного использования кода и позволяют делать это надежно и просто.

Простой пример обобщений

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

// Простой обобщенный класс.
// Здесь Т - это параметр типа, заменяемый именем
// подлинного типа при создании объекта класса Gen.

//В объявлении этого класса Т означает обобщенный тип.
class Gen<T> {
    Т ob; // объявить объект типа Т
    // передать конструктору ссылку на объект типа Т.
    Gen (Т о) {
        ob = о;
    }

    // возвратить объект ob из метода
    Т getob() {
        return ob;
    }

    // отобразить тип Т
    void showTypeO {
        System.out.println("Type of T is " +
                           ob.getClass().getName());
    }
}

// продемонстрировать обобщенный класс
class GenDemo {
    public static void main(String args[]) {
        // Создание ссылки на объект типа Gen<Integer>.
        Gen<Integer> iOb;

        // Создать объект типа Gen<Integer> и присвоить ссылку на
        // него переменной iOb. Обратите внимание на автоупаковку при
        // инкапсуляции значения 88 в объекте типа Integer.
        iOb = new Gen<Integer>(88); // получить экземпляр типа Gen<Integer>

        // отобразить тип данных, используемых в объекте iOb
        iOb.showType();

        // Получение значения из объекта iOb. Обратите внимание
        // на то,что приведение типов здесь не требуется,
        int v = iOb.getob();
        System.out.println("value: " + v);

        System.out.println();

        // Создание объекта типа Gen для символьных строк.
        // Здесь создается ссылка и сам объект типа Gen<String>.
        Gen<String> strOb = new Gen<String>("Generics Test");

        // отобразить тип данных, используемых в объекте strOb
        strOb.showType();

        // Получение значения из объекта strOb.
        //И здесь приведение типов не требуется.
        String str = strOb.getob();
        System.out.println("value: " + str) ;
    }
}

Выполнение данной программы дает следующий результат:

Type of Т is java.lang.Integer
value: 88
Type of Т is java.lang.String
value: Generics Test

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

class Gen<T> {

где Т — имя параметра типа. Это имя служит в качестве метки-заполнителя конкретного типа, который указывается при создании объекта класса Gen. Следовательно, имя т используется в классе Gen всякий раз, когда требуется параметр типа. Обратите внимание на то, что имя т заключается в угловые скобки (< >). Этот синтаксис можно обобщить: всякий раз, когда объявляется параметр типа, он указывается в угловых скобках. А поскольку параметр типа используется в классе Gen, то такой класс считается обобщенным.

В объявлении класса Gen можно указывать любое имя параметра типа, но по традиции выбирается имя Т. К числу других наиболее употребительных имен параметров типа относятся V и Е. А вообще, обозначать параметры типа рекомендуется одной прописной буквой.

Далее имя т используется для объявления объекта ob, как показано в следующей строке кода:

Т ob; // объявить объект типа Т

Как пояснялось выше, имя параметра типа т служит меткой-заполнителем конкретного типа, указываемого при создании объекта класса Gen. Поэтому объект ob будет иметь тип, передаваемый в качестве параметра типа т при получении экземпляра объекта класса Gen. Так, если качестве параметра типа Т указывается String, то экземпляр объекта оЪ будет отнесен к типу String.

Рассмотрим далее конструктор класса Gen.

Gen(Т о) {
    ob = о;
}

Как видите, параметр о этого конструктора относится к типу Т. Это означает, что конкретный тип параметра о определяется типом, передаваемым в качестве параметра типа Т при создании объекта класса Gen. А поскольку параметр о и переменная экземпляра ob относятся к типу Т, то после создания объекта класса Gen их конкретный тип окажется одним и тем же.

С помощью параметра типа т можно также указывать тип, возвращаемый методом, как показано ниже на примере метода getob().

Т getob() {
    return ob;
}

Переменная экземпляра ob также относится к типу т, поэтому ее тип совпадает с типом, возвращаемым методом getob().

Метод showType() отображает тип Т. С этой целью метод getName() вызывается для объекта типа Class, возвращаемого методом getClass(), вызываемым для объекта ob. Это средство еще не применялось в представленных до сих пор примерах программ, поэтому рассмотрим его подробнее. Как пояснялось в главе 7, в классе Object определен метод getClass(), автоматически являющийся членом каждого производного класса. Он возвращает объект типа Class, соответствующий типу класса текущего объекта. Класс Class относится к пакету java. lang и инкапсулирует сведения о текущем классе. В нем определено несколько методов, которые позволяют получать сведения о классах по ходу выполнения программы. К их числу принадлежит метод getName(), возвращающий строковое представление имени класса.

В классе Gen Demo демонстрируется применение обобщенного класса Gen. Прежде всего, в нем создается версия класса Gen для целых чисел, как показано ниже.

Gen<Integer> iOb;

Внимательно проанализируем это объявление. В первую очередь обратите внимание на то, что тип Integer указывается в угловых скобках после имени класса Gen. В данном случае Integer служит аргументом типа, передаваемым в качестве параметра типа Т класса Gen. В рассматриваемом здесь объявлении создается версия класса Gen, в которой тип Т заменяется типом Integer везде, где он встречается. Следовательно, после этого объявления Integer становится типом переменной ob и возвращаемым типом метода getob().

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

В следующей строке кода переменной iOb присваивается ссылка на экземпляр в версии класса Gen для типа Integer:

iOb = new Gen<Integer>(88);

Обратите внимание на то, что при вызове конструктора класса Gen указывается также аргумент типа Integer. Это необходимо потому, что тип объекта, на который указывает ссылка (в данном случае — iOb), должен соответствовать Gen. Если тип ссылки, возвращаемой оператором new, будет отличаться от Gen, возникнет ошибка при компиляции. Сообщение о такой ошибке будет, например, получено при попытке скомпилировать следующую строку кода:

iOb = new Gen<Double>(88.0); // Ошибка!

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

Как следует из комментариев к программе, в рассматриваемом здесь операторе присваивания

iOb = new Gen<Integer>(88);

производится автоупаковка целочисленного значения 88 в объект типа Integer. Это происходит потому, что обобщение Gen создает конструктор, которому передается аргумент типа Integer. А поскольку предполагается создание объекта типа Integer, то в нем автоматически упаковывается целочисленное значение 88. Разумеется, это можно было бы явно указать в операторе присваивания, как показано ниже.

iOb = new Gen<Integer>(new Integer(88));

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

Затем в программе отображается тип переменной ob в объекте iOb (в данном случае это тип Integer). А значение переменной ob получается в следующей строке кода:

int v = iOb.getobO;

Метод getob() возвращает значение типа Т, замененное на Integer при объявлении переменной ссылки на объект iOb, а следовательно, метод getob() фактически возвращает значение того же самого типа Integer. Это значение автоматически распаковывается перед присваиванием переменной v типа int.

И наконец, в классе GenDemo объявляется объект типа Gen.

Gen<String> strOb = new Gen<String>("Generics Test");

В этом объявлении указывается аргумент типа String, поэтому в объекте класса Gen вместо Т подставляется тип String. В итоге создается версия класса Gen для типаString, как демонстрируют остальные строки кода рассматриваемой здесь программы.

Действие обобщений распространяется только на объекты

При определении экземпляра обобщенного класса аргумент типа, передаваемый в качестве параметра типа, должен обозначать тип класса. Для этой цели нельзя использовать простой тип, например int или char. В примере с классом Gen в качестве параметра типа Т можно передать любой класс, но не простой тип данных. Иными словами, следующее объявление недопустимо:

Gen<int> strOb = new Gen<int>(53); // Ошибка. Использовать простой тип нельзя!

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

Различение обобщений по аргументам типа

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

iOb = strOb; // Ошибка!

Несмотря на то что обе переменные, iOb и strOb, относятся к типу Gen, они являются ссылками на объекты разного типа, поскольку при их объявлении указаны разные аргументы типа. Это часть той типовой безопасности обобщений, благодаря которой предотвращаются программные ошибки.

Обобщенный класс с двумя параметрами типа

В обобщенном классе можно задать несколько параметров типа. В этом случае параметры типа разделяются запятыми. Например, приведенный ниже класс TwoGen является переделанной версией класса Gen, в которой определены два параметра типа.

// Простой обобщенный класс с двумя параметрами типа: Т и V.
class TwoGen<T, V> { // Применение двух параметров типа
    Т оb1;
    V оb2;

    // передать конструктору класса ссылки на объекты типов Т и V
    TwoGen(Т ol, V о2) {.
        ob1 = ol;
        оb2 = о2;
    }

    // отобразить типы Т и V
    void showTypes() {
        System.out.println("Type of T is " +
                           obi.getClass().getName());
        System.out.println("Type of V is " +
                           ob2.getClass().getName());
    }

    T getobl() {
        return obi;
    }

    V getob2() {
        return ob2;
    }
}

// продемонстрировать класс TwoGen
class SimpGen {
    public static void main(String args[]) {
        // Здесь в качестве параметра типа Т передается тип
        // Integer, а в качестве параметра типа V - тип String.
        TwoGen<Integer, String> tgObj =
            new TwoGencinteger, String>(88, "Generics");

        // отобразить конкретные типы
        tgObj.showTypes();

        // получить и отобразить отдельные значения
        int v = tgObj.getobl();
        System.out.println("value: " + v);

        String str = tgObj.getob2();
        System.out.println("value: " + str);
    }
}

Выполнение этой программы дает следующий результат:

Type of Т is java.lang.Integer
Type of V is java.lang.String
value: 88
value: Generics

Обратите внимание на приведенное ниже объявление класса TwoGen.

class TwoGen<T, V> {

Здесь определяются два параметра типа, т и V, разделяемые запятыми. А поскольку в этом классе используются два параметра типа, то при создании его объекта следует непременно указывать оба аргумента типа, как показано ниже.

TwoGen<Integer, String> tgObj =
    new TwoGencinteger, String>(88, "Generics");

В данном случае тип Integer передается в качестве параметра типа т, а тип String — в качестве параметра типа V. И хотя в этом примере аргументы типа отличаются, они могут в принципе и совпадать. Например, следующая строка кода считается вполне допустимой:

TwoGen<String, String> х = new TwoGen<String, String>("A", "В");

В данном случае в качестве обоих параметров типа Т и V передается один и тот же тип String. Очевидно, что если аргументы типа совпадают, то определять два параметра типа в обобщенном классе нет никакой надобности.

Общая форма обобщенного класса

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

class имя_класса<список_параметров_типа> { II ...

А вот как выглядит синтаксис объявления ссылки на обобщенный класс:

имя_класса<список_аргументов_типа> имя_переменной =
    new имя_класса<список_аргументов_типа> (список_аргументов_конструктора) ;

Ограниченные типы

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

// Класс NumericFns как пример неудачной попытки создать
// обобщенный класс для выполнения различных математических
// операций, включая получение обратной величины или
// извлечение дробной части числовых значений любого типа,
class NumericFns<T> {
    Т num;

    // передать конструктору ссылку на числовой объект
    NumericFns(Т п) {
        num = п;
    }

    // возвратить обратную величину
    double reciprocal() {
        return 1 / num.doubleValue(); // Ошибка!
    }

    // возвратить дробную часть
    double fraction()   {
        return num.doubleValue() - num.intValue(); // Ошибка!
    }

    // ...
}

К сожалению, класс NumericFns в таком виде, в каком он приведен выше, не компилируется, так как оба метода, определенные в этом классе, содержат программную ошибку. Рассмотрим сначала метод reciprocal(), который пытается возвратить величину, обратную его параметру num. Для этого нужно разделить 1 на значение переменной num, которое определяется при вызове метода doubleValue(), возвращающего вариант double числового объекта, хранящегося в переменной num. Как известно, все числовые классы, в том числе Integer и Double, являются подклассами, производными от класса Number, в котором определен метод doubleValue(), что делает его доступным для всех классов оболочек числовых типов. Но дело в том, что компилятору неизвестно, что объекты класса NumericFns предполагается создавать только для числовых типов данных. Поэтому при попытке скомпилировать класс NumericFns возникает ошибка, а соответствующее сообщение уведомляет о том, что метод doubleValue() неизвестен. Аналогичная ошибка возникает дважды при компиляции метода fraction(), где вызываются методы doubleValue() и intValue(). При вызовах обоих этих методов компилятор также сообщает о том, что они неизвестны. Для того чтобы разрешить данное затруднение, нужно каким-то образом сообщить компилятору, что в качестве параметра типа Т предполагается передавать только числовые типы. И нужно еще убедиться, что в действительности передаются только эти типы данныхДля подобных случаев в Java предусмотрены ограниченные типы. При указании параметра типа можно задать верхнюю границу, объявив суперкласс, который должны наследовать все аргументы типа. И делается это с помощью оператора extends, указываемого при определении параметра типа, как показано ниже.

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

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

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

//В этой версии класса NumericFns аргументом типа,
// заменяющим параметр типа Т, должен стать класс Number
// или производный от него подкласс, как показано ниже,
class NumericFns<T extends Number> {
    T num;

    // передать конструктору ссылку на числовой объект
    NumericFns(Т п) {
        num = п;
    }

    // возвратить обратную величину
    double reciprocal() {
        return 1 / num.doubleValue() ;
    }

    // возвратить дробную часть
    double fraction()   {
        return num.doubleValue() - num.intValue();
    }

    // ...
}

// продемонстрировать класс NumericFns
class BoundsDemo {
    public static void main(String args[]) {

        // Применение класса Integer вполне допустимо, так как он
        // является подклассом, производным от класса Number.
        NumericFns<Integer> iOb =
            new NumericFns<Integer>(5) ;

        System.out.println("Reciprocal of iOb is " +
                           iOb.reciprocal());
        System.out.println("Fractional component of iOb is " +
                           iOb.fraction());

        System.out.println();

        // Применение класса Double также допустимо.
        NumericFns<Double> dOb =
            new NumericFns<Double>(5.25);

        System.out.println("Reciprocal of dOb is " +
                           dOb.reciprocal());
        System.out.println("Fractional component of dOb is " +
                           dOb.fraction());

        // Следующая строка кода не будет компилироваться, так как
        // класс String не является производным от класса Number.
        // NumericFns<String> strOb = new NumericFns<String>("Error");
    }
}

Ниже приведен результат выполнения данной программы.

Reciprocal of iOb is 0.2
Fractional component of iOb is 0.0

Reciprocal of dOb is 0.19047619047619047
Fractional component of dOb is 0.25

Как видите, для объявления класса NumericFns в данном примере служит следующая строка кода:

class NumericFns<T extends Number> {

Теперь тип т ограничен классом Number, а следовательно, компилятору Java известно, что для всех объектов типа т доступен метод doubleValue(), а также другие методы, определенные в классе Number. И хотя это само по себе дает немалые преимущества, кроме того, предотвращает создание объектов класса NumericFns для нечисловых типов. Так, если попытаться удалить комментарии из строки кода в конце рассматриваемой здесь программы, а затем повторно скомпилировать ее, то будет получено сообщение об ошибке, поскольку класс String не является подклассом, производным от класса Number.

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

// Тип V должен совпадать с типом Т или быть его подклассом.
class Pair<T, V extends Т> {
    Т first;
    V second;

    Pair(T a, V b) {
        first = a;
        second ='b;
    }

    // ...
}

В классе Pair определяются два параметра типа т и V, причем V расширяет тип Т. Это означает, что тип V должен быть либо того же типа, что и т, либо его подклассом. Благодаря такому объявлению гарантируется, что два параметра типа, передаваемые конструктору класса Pair, будут совместимы друг с другом. Например, приведенные ниже строки кода составлены правильно.

// Эта строка кода верна, так как Т и V относятся типу Integer.
Paircinteger, Integer> х = new Pair<Integer, Integer>(l, 2);

//И эта строка кода верна, так как Integer является подклассом Number.
Pair<Number, Integer> у = new Pair<Numbpr, Integer>(10.4, 12);

А следующий фрагмент кода содержит ошибку:

// Эта строка кода недопустима, так как String не является подклассом Number.
Pair<Number, String> z = new Pair<Number, String>(10.4, "12");

В данном случае класс String не является производным от класса Number, что нарушает граничное условие, указанное в объявлении класса Pair.

Использование метасимвольных аргументов

Несмотря на всю полезность типовой безопасности в обобщениях, иногда она может помешать использованию идеально подходящих языковых конструкций. Допустим, требуется реализовать метод absEqual(), возвращающий логическое значение true в том случае, если два объекта рассмотренного выше класса NumericFns содержат одинаковые абсолютные значения. Допустим также, что этот метод должен оперировать любыми типами числовых данных, которые могут храниться в сравниваемых объектах. Так, если один объект содержит значение 1,25 типа Double, а другой — значение -1,25 типа Float, метод absEqual() должен возвращать логическое значение true. Один из способов реализации метода absEqual() состоит в том, чтобы передавать этому методу параметр типа NumericFns, а затем сравнивать его абсолютное значение с абсолютным значением текущего объекта и возвращать логическое значение true, если эти значения совпадают. Например, вызов метода absEqual() может выглядеть следующим образом:

NumericFns<Double> dOb = new NumericFns<Double>(1.25) ;
NumericFns<Float> fOb = new NumericFns<Float>(-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<T> ob) {
    if(Math.abs(num.doubleValue()) ==
        Math.abs(ob.num.doubleValue()) return true;
    return false;
}

В данном случае для определения абсолютного значения каждого числа используется стандартный метод Math. abs(). Полученные значения сравниваются. Но дело в том, что рассматриваемое здесь решение окажется пригодным лишь в том случае, если объект класса NumericFns, передаваемый в качестве параметра, имеет тот же тип, что и текущий объект. Так, если текущий объект относится к типу NumericFns, параметр ob также должен быть типа NumericFns, а следовательно, сравнить текущий объект с объектом типа NumericFns не удастся. Таким образом, выбранное решение не является обобщенным.

Для того чтобы создать обобщенный метод absEqual(), придется воспользоваться еще одним свойством обобщений в Java, называемым метасимвольным аргументом. Для указания такого аргумента служит знак ?, обозначающий неизвестный тип данных. Используя метасимвольный аргумент, можно переписать метод absEqual() следующим образом:

// определить равенство абсолютных значений в двух объектах
boolean absEqual(NumericFns<?> ob) { // обратите внимание на метасимвол
    if(Math.abs(num.doubleValue()) ==
        Math.abs(ob.num.doubleValue()) return true;
    return false;
}

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

// Применение метасимвольного аргумента,
class NumericFns<T extends Number> {
    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<Integer> iOb =
            new NumericFns<Integer>(6) ;
        NumericFns<Double> dOb =
            new NumericFns<Double>(-6.0) ;
        NumericFns<Long> 10b =
            new NumericFns<Long>(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, а переменная dOb — на объект типа NumericFns. Благодаря применению ме- тасимвольного аргумента по ссылке на объект iOb удается передать объект dOb методу absEqual(). Подобным образом формируется и другой вызов, в котором методу передается объект типа NumericFns.

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

Ограниченные метасимвольные аргументы

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

class А {
    // ...
}

class В extends А {
    // ...
}

class С extends А {
    // ...
}

// Обратите внимание на то, что D не является подклассом А.
class D {
    // ...
}

Здесь класс А является суперклассом для классов В и С, но не для класса D.

Теперь рассмотрим очень простой обобщенный класс.

// Простой обобщенный класс.
class Gen<T> {  ^
    Т 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<A> w = new Gen<A>(a);
        Gen<B> w2 = new Gen<B>(b);
        Gen<C> w3 = new Gen<C>(c);
        Gen<D> w4 = new Gen<D>(d);

        // Эти вызовы метода test() допустимы, так как
        // объекты w, w2 и w3 относятся к подклассам А.
        test(w);
        test(w2);
        test(w3);

        //А этот вызов метода test() недопустим, так как
        // объект не относится к подклассу Л.
        // test(w4); // Ошибка!
    }
}

В методе main() создаются объекты классов А, В, С и D. Затем они используются для создания четырех объектов класса Gen (по одному на каждый тип). После этого метод test() вызывается четыре раза, причем последний его вызов закомментирован. Первые три вызова вполне допустимы, поскольку w, w2 и w3 являются объектами класса Gen, типы которых определяются^ классом А или производными от него классами. А последний вызов метода test() недопустим, потому что w4 — это объект класса D, не являющегося производным от к класса А. Следовательно, ограниченный метасимвольный аргумент в методе test() не позволяет передавать ему объект w4 в качестве параметра.

В целом верхняя граница для метасимвольного аргумента задается в следующей общей форме:

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

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

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

<? extends подкласс >

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

В этом случае следующее приведение типов может быть выполнено, поскольку переменная х указывает на экземпляр класса Gen:

(Gen<Integer>) х // Допустимо

А следующее приведение типов не может быть выполнено, поскольку переменная х не указывает на экземпляр класса Gen:

(Gen<Long>) х // Недопустимо

Обобщенные методы

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

Ниже приведен пример программы, в которой объявляется класс 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;

    // Обобщенный конструктор.
    <T extends Number> Summation(T arg) {
        sum = 0;

        for(int i=0; i <= arg.intValue(); i++)
            sum += i;
    }

    int getSum() {
        return 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<T> { // обобщенный интерфейс
    // Метод contains() проверяет, содержится ли
    // некоторый элемент в объекте класса,
    // реализующего интерфейс Containment,
    boolean contains(Т о);
}

// реализовать интерфейс Containment с помощью массива,
// предназначенного для хранения значений.
// Любой класс, реализующий обобщенный интерфейс,
// также должен быть обобщенным.
class MyClass<T> implements Containment<T> {
    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<Integer> ob = new MyClass<Integer>(x);
        if(ob.contains(2))
            System.out.println("2 is in ob");
        else
            System.out.println("2 is NOT in ob");

        if(ob.contains(5))
            System.out.println("5 is in ob");
        else
            System.out.println("5 is NOT in ob");
        // Следующие строки кода недопустимы, так как объект ob
        // является вариантом реализации интерфейса Containment для
        // типа Integer, а значение 9.25 относится к типу Double.
        // if(ob.contains(9.25)) // Недопустимо!
        // System.out.println("9.25 is in ob"); ~
    }
}

Выполнение этой программы дает следующий результат:

2 is in ob
5 is NOT in ob

Большую часть исходного кода этой программы нетрудно понять, но на некоторых ее особенностях следует все же остановиться. Обратите прежде всего внимание на то, как объявляется интерфейс Containment:

interface Containment<T> {

Обобщенные интерфейсы объявляются таким же образом, как и обобщенные классы. В данном случае параметр типа Т задает тип включаемого объекта. Интерфейс Containment реализуется классом MyClass. Объявление этого класса выглядит следующим образом:

class MyClass<T> implements Containment<T> {

Если класс реализует обобщенный интерфейс, то он также должен быть обобщенным. В нем должен быть объявлен как минимум тот же параметр типа, который указан в объявлении интерфейса. Например, приведенный ниже вариант объявления класса MyClass недопустим.

class MyClass implements Containment<T> { // Ошибка!

В данном случае ошибка заключается в том, что в классе MyClass не объявлен параметр типа, а это означает, что передать параметр типа интерфейсу Containment нельзя. Если идентификатор Т останется неизвестным, компилятор выдаст сообщение об ошибке. Класс, реализующий обобщенный интерфейс, может не быть обобщенным только в одном случае: если при объявлении класса для интерфейса указывается конкретный тип. Такой способ объявления класса приведен ниже,

class MyClass implements Containment<Double> { // Допустимо

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

interface Containment<T extends Number> {

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

class MyClass<T extends Number> implements Containment<T> {

Обратите особое внимание на то, как параметр типа Т объявляется в классе MyClass, а затем передается интерфейсу Containment. На этот раз интерфейсу Containment требуется тип, расширяющий тип Number, поэтому в классе MyClass, реализующем этот интерфейс, должны быть указаны соответствующие ограничения. Если верхняя граница задана в объявлении класса, то ее нет необходимости указывать еще раз в операторе implements. Если же попытаться сделать это, будет получено сообщение об ошибке. Например, следующее выражение составлено неверно и не будет скомпилировано:

// Ошибка!
class MyClass<T extends Number>
    implements Containment<T extends Number> {

Если параметр типа задан в объявлении класса, он лишь передается интерфейсу без дальнейших видоизменений.

Ниже приведена общая форма объявления обобщенного интерфейса.

interface имя_интерфейса<параметры_типа> { // ...

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

class имя_класса<параметры_типа>
    implements имя_интерфейса<параметры_типа> {

Пример для опробования 13.1. Создание обобщенного класса очереди

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

В этом проекте предстоит в очередной и последний раз видоизменить класс очереди, разработка которого была впервые начата в главе 5. Для этой цели будет объявлен обобщенный интерфейс, определяющий операции над очередью, созданы два класса исключений и реализована очередь фиксированного размера. Разумеется, вам ничто не помешает поэкспериментировать с другими разновидностями обобщенных очередей, например, создать динамическую или циклическую очередь, следуя приведенным ниже рекомендациям.

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

Последовательность действий

  1. Первым этапом создания обобщенной очереди станет формирование обобщенного интерфейса, описывающего две операции над очередью: размещение и извлечение. Обобщенная версия интерфейса очереди будет называться iGenQ, ее исходный код приведен ниже. Введите этот код во вновь созданный файл IGenQ. java.

    // Обобщенный интерфейс очереди,
    public interface IGenQ<T> {
        // поместить элемент в очередь
        void put(T ch) throws QueueFullException;
    
        // извлечь элемент из очереди
        Т get() throws QueueEmptyException;
    }
    

    Обратите внимание на то, что тип данных, предназначенных для хранения в очереди, определяется параметром типа т.

  2. Далее создайте файл 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.";
        }
    }
    

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

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

    // Обобщенный класс, реализующий очередь фиксированного размера,
    class GenQueue<T> implements IGenQ<T> {
        private T q[];  //  Массив для хранения элементов очереди,
        private int putloc, getloc; // Индексы размещения и извлечения
                                    // элементов очереди.
        // построить пустую очередь из заданного массива
        public GenQueue(Т[] aRef) {
            q = aRef;
            putloc = getloc = 0;
        }
    
        // поместить элемент в  очередь
        public void put(Т obj)
        throws QueueFullException {
            if(putloc==q.length-1)
                throw new QueueFullException(q.length-1);
    
            putloc++;
            q[putloc] = obj;
        }
        // извлечь элемент из очереди
        public Т get()
            throws QueueEmptyException {
    
            if(getloc == putloc)
                throw new QueueEmptyException();
    
            getloc++;
            return q[getloc];
        }
    }
    

    Класс GenQueue объявляется как обобщенный с параметром типа Т. Этот параметр определяет тип данных, хранящихся в очереди. Обратите внимание на то, что параметр типа Т также передается интерфейсу iGenQ.

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

    String strArray[] = new String[10];
    GenQueue<String> strQ = new GenQueue<String>(strArray);
    
  4. Создайте файл GenQDemo.java и введите в него приведенный ниже код. В этом коде демонстрируется работа обобщенной очереди.

    /*
    Проект 13.1.
    
    Демонстрация обобщенного класса очереди.
    */
    class GenQDemo {
        public static void main(String args[])  {
            // создать очередь для хранения целых чисел
            Integer iStoref] = new Integer[10];
            GenQueue<Integer> q = new GenQueue<Integer>(iStore);
    
            Integer iVal;
            System.out.println("Demonstrate a queue of Integers.");
            try {
                for(int i=0; i < 5; i++) {
                    System.out.println("Adding " + i + " to the q.");
                    q.put(i); // ввести целочисленное значение в очередь q
                }
            }
            catch (QueueFullException exc) {
                System.out.println(exc) ;
            }
            System.out.println();
            try {
                for(int i=0; i < 5; i++) {
                    System.out.print("Getting next Integer from q: ");
                    iVal = q.get();
                    System.out.println(iVal);
                }
            }
            catch (QueueEmptyException exc) {
                System.out.println(exc);
            }
    
            System.out.println() ;
    
            // создать очередь для хранения чисел с плавающей точкой
            Double dStore[] = new Double[10];
            GenQueue<Double> q2 = new GenQueue<Double>(dStore);
            Double dVal;
    
            System.out.println("Demonstrate a queue of Doubles.");
            try {
                for(int i=0; i < 5; i++) {
                    System.out.println("Adding " + (double)i/2 +
                                       " to the q2.");
                    q2.put((double)i/2); // ввести значение типа double в очередь q2
                }
            }
            catch (QueueFullException exc) {
                System.out.println(exc);
            }
            System.out.println();
    
            try {
                for(int i=0; i < 5; i++) {
                    System.out.print("Getting next Double from q2: ");
                    dVal = q2.get();
                    System.out.println(dVal);
                }
            }
            catch (QueueEmptyException exc) {
             System.out.println(exc);
            }
        }
    }
    

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

    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<T> {
    Т ob; // объявить объект типа Т

    // передать конструктору ссылку на объект типа Т
    Gen(Т о) {
        ob = о;
    }

    // возвратить объект ob
    Т getob() {
        return ob;
    }
}

// продемонстрировать использование базового типа
class RawDemo {
    public static void main(String args[]) {

        // создать объект класса Gen для типа Integer
        Gen<Integer> iOb = new Gen<Integer>(88);

        // создать объект класса Gen для типа String
        Gen<String> strOb = new Gen<String>("Generics Test");

        // создать объект класса Gen для базового типа
        // и передать ему значение типа Double
        // Если аргумент типа не передается, создается базовый тип.
        Gen raw = new Gen(new Double(98.6));

        // Здесь требуется приведение типов, так как тип неизвестен,
        double d = (Double) raw.getob();
        System.out.println("value: " + d);

        // Использование базового типа может привести
        //к исключениям при выполнении программы.
        // Ниже представлен ряд тому примеров.

        // Следующее приведение типов вызывает ошибку
        // при выполнении программы!
        // int i = (Integer) raw.getob();

        // Следукяцее присваивание чревато ошибкой.
        // Базовые типы отменяют типовую безопасность.
        strOb = raw; // Допустимо,    но  потенциально ошибочно.
        // String   str = strOb.getob();    //  Ошибка при  выполнении  программы.

        // Следующее присваивание также чревато ошибкой,
        raw = iOb; // Допустимо, но потенциально  ошибочно.
        // d = (Double) raw.getob();    //  Ошибка при  выполнении  программы.
    }
}

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

Gen raw = new Gen(new Double(98.6));

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

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

Недостаточный уровень типовой безопасности демонстрируют примеры в закомментированных строках кода в конце данной программы. Рассмотрим их по отдельности. Сначала проанализируем следующую строку кода:

// int i = (Integer) raw.getobO;

В этом операторе присваивания в объекте raw определяется значение переменной ob, которое приводится к типу Integer. Но дело в том, что в объекте raw содержится не целое число, а значение типа Double. На стадии компиляции этот факт выявить невозможно, поскольку тип объекта raw неизвестен. Следовательно, ошибка возникнет на стадии выполнения программы.

В следующих строках кода ссылка на объект класса Gen базового типа присваивается переменной strOb (ссылки на объект типа Gen).

strOb = raw; // Допустимо, но потенциально ошибочно.
// String str = strOb.getob(); // Ошибка при выполнении программы.

Само по себе присваивание синтаксически правильно, но логически сомнительно. Переменная strOb ссылается на объект типа Gen, а следовательно, она должна содержать ссылку на объект, содержащий значение типа String, но после присваивания объект, на который ссылается переменная strOb, содержит значение типа Double. Поэтому при выполнении программы, когда предпринимается попытка присвоить переменной str содержимое объекта, на который ссылается переменная strOb, возникает ошибка. А причина ее заключается в том, что переменная strOb ссылается в этот момент на объект, содержащий значение типа Double. Таким образом, присваивание ссылки на объект базового типа переменной ссылки на объект обобщенного типа делается в обход механизма типовой безопасности.

В следующих строках кода демонстрируется ситуация, обратная предыдущей:

raw = iOb; // Допустимо, но потенциально ошибочно.
// d = (Double) raw.getobO; // Ошибка при выполнении программы.

В данном случае ссылка на объект обобщенного типа присваивается переменной базового типа. И это присваивание синтаксически правильно, но приводит к ошибке, возникающей во второй строке кода. В частности, переменная raw указывает на объект, содержащий значение типа Integer, но при приведении типов предполагается, что он содержит значение типа Double. И эту ошибку также нельзя выявить на стадии компиляции, а проявляется она при выполнении программы.

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

Gen raw = new Gen(new Double(98.6));

strOb = raw; // Допустимо, но потенциально ошибочно.

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

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

raw = iOb; // Допустимо, но потенциально ошибочно.

В этом случае компилятор не предупреждает пользователя, потому что такое присваивание не приводит к еще большей потере типовой безопасности по сравнению с той, которая произошла при создании переменной raw базового типа.

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

Выводимость типов с помощью ромбовидного оператора

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

class TwoGencT, V> {
    Т obi;
    V ob2;
    // передать конструктору ссылку на объект типа Т.
    TwoGen(Т о1, V о2)  {
        obi = ol;
        оb2 = о2;
    }
    // ...
}

До появления версии JDK 7 для создания экземпляра класса TwoGen приходилось прибегать к оператору присваивания, аналогичному приведенному ниже.

TwoGen<Integer, String> tgOb =
    new TwoGencinteger, String>(42, "testing");

Здесь аргументы типа (в данном случае — Integer и String) указываются дважды: сначала при объявлении переменной tgOb, а затем при создании экземпляра класса TwoGen с помощью оператора new. Начиная с версии JDK 5, в которой были внедрены обобщения, подобная форма создания объектов обобщенного типа требовалась при написании всех программ на Java, вплоть до появления версии JDK 7. И хотя в этой форме, по существу, нет ничего ошибочного, все же она выглядит несколько многословной. Ведь в операторе new типы аргументов могут быть выведены без особого, труда, и поэтому нет никаких оснований указывать их еще раз. И для этой цели в версии JDK 7 внедрен специальный синтаксический элемент.

В версии JDK 7 приведенное выше объявление может быть переписано следующим образом:

TwoGen<Integer, String> tgOb = new TwoGen<>(42, "testing");

Как видите, в той части, где создается экземпляр объекта обобщенного типа, просто указываются угловые скобки (< >), обозначающие пустой список аргументов типа. Это так называемый ромбовидный оператор, предписывающий компилятору вывести типы аргументов, требующиеся конструктору в операторе new. Главное преимущество такого синтаксиса выведения типов состоит в том, что он позволяет сделать более краткими те операторы объявления, которые порой выглядят слишком длинными и громоздкими. Это оказывается особенно удобным для объявления обобщенных типов, определяющих границы наследования в иерархии классов Java.

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

class-namе<аргументы_типа> имя_переменной =
    new class-name< >(аргументы_конструктора) ;

Здесь список аргументов типа в операторе new указывается пустым.

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

boolean isSame(TwoGen<T, V> о) {
    if(obi == о.obi && ob2 == o.ob2) return true;
    else return false;
}

то приведенный ниже вызов окажется вполне допустимым в версии JDK 7.

if(tgOb.isSame(new TwoGen<>(42, "testing"))) System.out.println("Same");

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

Ромбовидный оператор является новым языковым средством в версии JDK 7, но непригодным для компиляции кода, написанного в предыдущих версиях Java. Поэтому в примерах программ, приведенных далее в этой книге, будет использоваться прежний несокращенный синтаксис объявления экземпляров обобщенных классов, чтобы эти программы можно было скомпилировать любым компилятором Java, поддерживающим обобщения. Кроме того, несокращенный синтаксис объявления экземпляров обобщенных классов яснее дает понять, что именно создается, и благодаря этому представленные в книге примеры становятся более наглядными и полезными. Разумеется, в своих программах на Java вы вольны пользоваться синтаксисом выведения типов, чтобы упростить их объявления.

Стирание

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

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

Механизм стирания действует следующим образом. При компиляции кода, написанного на Java, все сведения об обобщенных типах удаляются, т.е. стираются. Это означает, что параметры типа заменяются верхними границами их типа, а если границы не указаны, то их функции выполняет класс Object. После этого производится приведение типов, определяемых аргументами типа. Подобная совместимость типов соблюдается компилятором. Благодаря такому подходу к реализации обобщений параметры типа при выполнении программы вообще отсутствуют, но действуют только на стадии компиляции исходного кода.

Ошибки неоднозначности

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

// Неоднозначность, вызванная стиранием перегруженных методов,
class MyGenClass<T, V> {
    Т obi;
    V ob2;

    // ...

    // Два следующих метода конфликтуют друг с другом,
    // поэтому код не компилируется.
    // Эти методы существенно неоднозначны.
    void set(T о) {
        obi = о;
    }

    void set(V о) {
        ob2 = о;
    }
}

В классе MyGenClass объявлены два обобщенных типа: Т и V. В этом классе предпринимается попытка перегрузить метод set(). Перегружаемые методы отличаются параметрами типа т и V. Казалось бы, это не должно приводить к ошибке, поскольку типы Т и V отличаются. Но здесь возникают два затруднения в связи с неоднозначностью.

Прежде всего, в классе MyGenClass не указано никаких требований, чтобы типы Т и V действительно отличались. В частности, не является принципиальной ошибкой создание объекта типа MyGenClass так, как показано ниже.

MyGenClass<String, String> obj = new MyGenClass<String, String>()

В данном случае типы Т и V заменяются типом String. В результате оба варианта метода set() становятся совершенно одинаковыми, что, безусловно, считается ошибкой.

Второе, более серьезное затруднение возникает в связи с тем, что при стирании обобщенных типов оба варианта метода set() преобразуются в следующий вид:

void set(Object о) { // ...

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

Ограничения, накладываемые на обобщения

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

Невозможность получить экземпляры параметров типа

Экземпляр параметра типа получить невозможно. Рассмотрим в качестве примера следующий класс:

// Экземпляр типа Т получить нельзя,
class Gen<T> {
    Т ob;
    Gen()  {
    ob = new T(); // Недопустимо!!!
    }
}

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

Ограничения, накладываемые на статические члены класса

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

class Wrong<T> {
    // Статическую переменную типа Т создать нельзя,
    static Т ob;

    // В статическом методе нельзя использовать тип Т.
    static Т getob()    {
        return ob;
    }

    // Статический метод не может обращаться к объекту типа Т.
    static void showob() {
        System.out.println(ob);
    }
}

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

Ограничения, накладываемые на обобщенные массивы

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

// Обобщенные типы и массивы,
class Gen<T extends Number> {
    T ob;

    T vals[]; // Допустимо.

    Gen(T о, T[] nums) {
        ob = о;

        // Следующее выражение недопустимо:
        // vals = new Т[10]; // Нельзя создать массив типа Т.

        // Следующее выражение составлено верно.
        vals = nums; // Переменной допускается присваивать ссылку
        // на существующий массив.
    }
}

class GenArrays {
    public static void main(String args[])  {
        Integer n[] = { 1, 2, 3, 4, 5 };

        Gen<Integer> iOb = new Gen<Integer>(50, n);

        // Нельзя создать массив обобщенных ссылок
        // на объекты конкретного типа.
        // Gen<Integer> gens[] = new Gen<Integer>[10]; // Ошибка!

        // Следующее выражение составлено верно.
        Gen<?> gens[] = new Gen<?>[10];
    }
}

Как следует из исходного кода приведенной выше программы, допускается создавать ссылку на массив типа т. Это демонстрируется в следующей строке кода:

Т vals[]; // Допустимо.

Но получить экземпляр самого массива типа т нельзя. Именно поэтому приведенная ниже строка кода закомментирована.

// vals = new Т[10]; // Нельзя создать массив типа Т.

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

vals = nums; // Переменной допускается присваивать ссылку
             // на существующий массив.

Это выражение составлено верно, поскольку тип массива, передаваемого конструктору Gen() при создании объекта, известен и совпадает с типом Т. В теле метода main() содержится выражение, демонстрирующее невозможность объявить массив обобщенных ссылок на объекты конкретного типа. Поэтому приведенная ниже строка кода не будет скомпилирована.

// Gen<Integer> gens[] = new Gen<Integer>[10] ; // Ошибка!

Ограничения, накладываемые на обобщенные исключения

Обобщенный класс не может расширять класс Throwable. Это означает, что создавать обобщенные классы исключений нельзя.

Дальнейшее изучение обобщений

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

Читатели, которых заинтересовала эта тема, вероятно, захотят узнать побольше о томвлиянии, которое обобщения оказывают на иерархию классов и, в частности, каким образом осуществляется сравнение типов при выполнении программы, как переопределяются методы и т.д. Все эти и многие другие вопросы употребления обобщений подробно освещены в книге Java. Полное руководство, 8-е издание, ИД “Вильямс”, 2012 г.

Упражнение для самопроверки по материалу главы 13

  1. Обобщения очень важны, поскольку они позволяют создавать код, который: a) обеспечивает типовую безопасность; b) пригоден для повторного использования; c) отличается высокой надежностью; d) обладает всеми перечисленными выше свойствами.
  2. Можно ли указывать простой тип в качестве аргумента типа?
  3. Как объявить класс FlightSched с двумя параметрами типа?
  4. Измените ваш ответ на вопрос 3 таким образом, чтобы второй параметр типа обозначал подкласс, производный от класса Thread.
  5. Внесите изменения в класс FlightSched таким образом, чтобы второй параметр типа стал подклассом первого параметра типа.
  6. Что обозначает знак ? в обобщениях?
  7. Может ли метасимвольный аргумент быть ограниченным?
  8. У обобщенного метода My Gen() имеется один параметр типа, определяющий тип передаваемого ему аргумента. Этот метод возвращает также объект, тип которого соответствует параметру типа. Как должен быть объявлен метод My Gen() ?
  9. Допустим, обобщенный интерфейс объявлен так, как показано ниже.
    interface IGenIF<T, V extends Т> { // ...
    
    Составьте объявление класса MyClass, который реализует интерфейс iGenlF.
  10. Допустим, имеется обобщенный класс Counter. Как создать объект его базового типа?
  11. Существуют ли параметры типа на стадии выполнения программы?
  12. Видоизмените ответ на вопрос 10 в упражнении по материалу главы 9 таким образом, чтобы сделать класс обобщенным. По ходу дела создайте интерфейс стека iGenStack, объявив в нем обобщенные методы push() и pop().
  13. Что обозначают угловые скобки (< >)?
  14. Как упростить приведенную ниже строку кода в версии JDK 7?
    MyClass<Double,String> obj = new MyClass<Double,String>(1.1,"Hi");