Java Урок 61: ОБОБЩЕНИЯ, обобщенный класс

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

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

Классы, интерфейсы или методы, имеющие дело с параметризованными типами, называются обобщениями, обобщенными классами или обобщенными методами.

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

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

// Простой обобщенный класс.
// Здесь Т — это параметр типа,
// который будет заменен реальным типом
// при создании объекта класса Gen.
class Gen<T> {
   Т ob; // объявление объекта типа Т

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

   // Вернуть ob.
   Т getobO {
      return ob;
   }

   // Показать тип Т.
   void showType() {
      System.out.println("Типом T является " + ob.getClass().getName());
   }
}

// Демонстрация обобщенного класса.
class GenDemo {
   public static void main(String args[]) {
      // Создать объект Gen<Integer> и присвоить
      // ссылку на iOb. Отметьте применение автоупаковки
      // для инкапсуляции значения 88 в объект Integer.
      Gen<Integer> iOb;
      iOb = new Gen<Integer>(88);

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

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

      // Создать объект Gen для String.
      Gen<String> strOb = new Gen<String> ("Обобщенный тест");

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

      // Получить значение strOb. Опять же
      // приведение не требуется.
      String str = strOb.getob();

      System.out.println("Значение: " + str);
   }
}

Результат работы этой программы:
Типом Т является java.lang.Integer
Значение: 88
Типом Т является java.lang.String
Значение: Обобщенный тест

Давайте внимательно исследуем эту программу. Обратите внимание на объявление класса Gen в следующей строке.

class Gen<T> {

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

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

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

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

Теперь рассмотрим конструктор Gen ():

Gen(Т о) {
   ob = о;
}

Как видите, параметр о имеет тип Т. Это значит, что реальный тип параметра о определяется типом, переданным параметром типа Т при создании объекта класса Gen.
К тому же, поскольку и параметр о, и переменная-член ob имеют тип Т, они оба получают одинаковый реальный тип при создании объекта класса Gen. Параметр типа Т также может быть использован для указания типа возвращаемого значения метода, как в случае метода getob (), оказанного здесь:

Т getob() {
   return ob;
}

Так как объект ob тоже имеет тип Т, его тип совместим с типом, возвращаемым методом getob().

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

Класс GenDemo демонстрирует обобщенный класс Gen. Сначала он создает версию класса Gen для целых чисел, как показано ниже:

Gen<Integer> iOb;

Посмотрим на это объявление внимательней. Отметим, что тип Integer указан в угловых скобках после слова Gen. В этом случае Integer — это аргумент типа, который передается в параметре типа Т класса Gen. Это фактически создает версию класса Gen, в которой все ссылки на тип Т транслируются в ссылки на тип Integer.

То есть в данном объявлении объект ob имеет тип Integer, и тип
возвращаемого значения метода getob() также имеет тип Integer.

Следующая строка присваивает объекту iOb ссылку на экземпляр целочисленной версии класса Gen:

iOb = new Gen<Integer>(88);

Отметим, что когда вызывается конструктор Gen(), аргументтипа Integer также указывается. Это необходимо, потому что типом объекта (в данном случае объекта iOb, которому присваивается ссылка, является тип Gen.

То есть ссылка, возвращаемая оператором new, также должна иметь тип Gen.
Если это не так, получается ошибка времени компиляции.
Например, следующее присваивание вызовет ошибку компиляции:

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

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

iOb = new Gen<Integer>(88);

Как указано в комментарии к программе, присваивание использует автоупаковку для инкапсуляции значения 88, имеющего тип int, в объекте класса Integer.
Это работает, потому что тип Gen создает конструктор, принимающий аргумент класса Integer. Поскольку ожидается объект класса Integer, Java автоматически упаковывает 88 внутрь него. Конечно, присваивание также может быть написано явно, как здесь:

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

Однако с этой версией не связано никаких преимуществ. Программа затем отображает тип объекта ob внутри объекта iOb, которым является Integer.
Далее программа получает значение объекта ob в следующей строке.

int v = iOb.getobO;

Поскольку возвращаемым типом метода getob() будет Т, который заменяется на Integer при объявлении объекта iOb, то возвращаемым типом метода getob() также будет класс Integer, который автоматически распаковывается в тип int и присваивается переменной v, имеющей тип int. То есть нет никакой необходимости приводить тип возвращаемого
значения метода getob() к классу Integer. Конечно, использовать автоупаковку не обязательно. Предыдущая строка может быть написана так:

int v = iOb.getob().intValue();

Однако автоупаковка позволяет сделать код более компактным.

Далее в классе GenDemo объявляется объект типа Gen:

Gen<String> strOb = new Gen<String>(" Обобщенный тест");

Поскольку аргументом типа является String, класс String подставляется вместо параметра Т внутри класса Gen. Это создает (концептуально) строковую версию класса Gen, что и демонстрируют остальные строки программы.

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

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

Gen<int> intOb = new Gen<int>(53); // Ошибка, нельзя использовать
// элементарные типы

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

Отличие обобщенных типов в зависимости от аргументов типа.

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

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

iOb = strOb; //Не верно!

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

Обобщения повышают безопасность типов.

Теперь вы можете задать себе следующий вопрос: если те же функциональные возможности, которые мы обнаружили в обобщенном классе Gen, могут быть получены без обобщений, т.е. простым указанием класса Object в качестве типа данных и применением правильных приведений, то в чем же выгода от того, что класс Gen параметризован?

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

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

// NonGen — функциональный эквивалент Gen,
// не использующий обобщений,
class NonGen {
   Object ob; // ob теперь имеет тип Object

   // Передать конструктору ссылку на объект типа Object
   NonGen(Object о) {
      ob = о ;
   }
   // Вернуть тип Object.
   Object getob() {
      return ob;
   }
   // Показать тип ob.
   void showType() {
      System.out.println("Типом ob является " + ob.getClass().getName());
   }
}

// Демонстрация необобщенного класса,
class NonGenDemo {
   public static void main(String args[]) {

      // Создать объект NonGen и сохранить
      // Integer в нем. Автоупаковка используется.
      NonGen iOb;
      iOb = new NonGen(88);

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

      // Получить значение iOb.
      // На этот раз приведение необходимо,
      int v = (Integer) iOb.getobO;
      System.out.println("значение: " + v);
      System.out.println();

      // Создать другой объект NonGen и
      // сохранить в нем String.
      NonGen strOb = new NonGen("Тест без обобщений");
      // Показать тип данных, используемый strOb.
      strOb.showType();

      // Получить значение strOb.
      // Опять же — приведение необходимо.
      String str = (String) strOb.getobО;
      System.out.println("Значение: " + str);

      // Это компилируется, но концептуально неверно!
      iOb = strOb;
      v = (Integer) iOb.getobO; // ошибка времени выполнения!
   }
}

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

Однако это не позволяет компилятору Java иметь какую-то реальную информацию о типе данных, в действительности сохраняемых в объекте
класса NonGen, что плохо по двум причинам.

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

int v = (Integer) iOb.getobO;

Поскольку возвращаемым типом метода getob() является тип Object, необходимо привести его к типу Integer, чтобы позволить выполнить автораспаковку и сохранить значение в переменной v. Если убрать приведение, программа некомпилируется.
В версии с обобщением приведение происходит неявно. В версии
без обобщения приведение должно быть явным. Это не только приносит неудобство, но и является потенциальным источником ошибок.

Теперь рассмотрим следующую кодовую последовательность в конце программы:

// Это компилируется, но концептуально неверно!
iOb = strOb;
v = (Integer) iOb.getob(); // ошибка времени выполнения!

Здесь объект strOb присваивается объекту iOb. Однако объект strOb ссылается на объект, содержащий строку, а не целое число. Это присваивание синтаксически корректно, потому что все ссылки класса NonGen одинаковы и любая ссылка класса NonGen может указывать на любой другой объект типа NonGen.

Однако этот оператор семантически неверен, что и отражено в следующей строке.

Здесь тип возвращаемого значения метода getob() приводится к классу Integer, а затем делается попытка присвоить это значение переменной v.

Проблема в том, что объект iOb теперь ссылается на объект, который хранит тип String, а не Integer.

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

Возможность создавать безопасный в отношении типов код, в котором ошибки несоответствия типов перехватываются компилятором — это главное преимущество обобщений. Хотя использование ссылок на тип Object для создания “псевдообобщенного” кода всегда возможно, нужно помнить, что такой код не является безопасным в отношении типов, и злоупотребление им приводит к исключениям времени выполнения.
Обобщения предотвращают подобные вещи. По сути, благодаря обобщениям, ошибки времени выполнения преобразуются в ошибки времени компиляции. Это и есть главное преимущество.

Введите свой email адрес для того, чтобы подписаться на мой блог:


knopkisoc

Добавить комментарий