Рассмотрим следующий пример знакомой структуры классов, использующей полиморфизм. Общий тип — это класс Shape
, а специализированные производные типы — это Circle
, Square
и Triangle
.
Это типичная схема классовой структуры, где базовый класс находится в вершине, а производные классы располагаются ниже. Основной целью объектно-ориентированного программирования является управление ссылками на базовый тип (в данном случае Shape
) большим количеством кода таким образом, чтобы добавление нового типа (например, Rhomboid
, производного от Shape
) не влияло на существующий код. В этом примере динамически связываемый метод интерфейса Shape
— это draw()
. Поэтому клиентский программист вызывает draw()
через обычную ссылку на Shape
. Метод draw()
переопределяется во всех производных классах, и поскольку он является динамически связываемым методом, то даже при вызове его через обычную ссылку на Shape
он будет правильно работать. Это и есть действие полиморфизма.
Поэтому обычно создается конкретный объект (Circle
, Square
или Triangle
), который затем преобразуется в Shape
(игнорируя специальный тип объекта), после чего используется анонимная ссылка на Shape
в остальной части программы.Как краткий обзор полиморфизма и приведения типов, можно закодировать вышеупомянутый пример следующим образом (если возникают трудности при выполнении этого примера, обратитесь к разделу 3.1.2 главы 3 "Присваивание").```java
//: Shapes.java
package c11;
import java.util.*;
interface Shape { void draw(); }
class Circle implements Shape { public void draw() { System.out.println("Круг.Отрисовать()"); } }
class Square implements Shape { public void draw() { System.out.println("Квадрат.Отрисовать()"); } }
class Triangle implements Shape { public void draw() { System.out.println("Треугольник.Отрисовать()"); } }
public class Shapes {
public static void main(String[] args) {
Vector s = new Vector();
s.addElement(new Circle());
s.addElement(new Square());
s.addElement(new Triangle());
Enumeration e = s.elements();
while(e.hasMoreElements())
((Shape)e.nextElement()).draw();
}
} ///:~
```Базовый класс может быть представлен как интерфейс (interface
), абстрактный класс (`abstract`) или обычный класс. Поскольку `Shape` не имеет истинных членов (то есть членов с определением), и нам не важно, был ли создан чисто абстрактный `Shape` объект, наиболее подходящим и гибким способом представления является использование интерфейса. Кроме того, отсутствие необходимости указывать все эти ключевые слова `abstract` делает весь код более чистым. Каждый производный класс переопределяет метод `draw` базового класса, поэтому они имеют различное поведение. В методе `main()` создаются объекты конкретных типов `Shape`, а затем добавляются в `Vector`. Именно здесь происходит приведение типа "вверх", так как `Vector` может содержать только объекты. Поскольку все в Java (кроме примитивных типов данных) являются объектами, `Vector` также может содержать объекты `Shape`. Однако при приведении типа до `Object` любая специфическая информация теряется, включая тот факт, что объект является геометрической фигурой. Для `Vector` эти объекты просто являются `Object`.При извлечении элемента из `Vector` с помощью `nextElement()`, ситуация становится немного сложнее. Поскольку `Vector` содержит только объекты, `nextElement()` естественно возвращает ссылку на `Object`. Однако мы знаем, что это фактически ссылка на `Shape`, и хотим отправить сообщение `Shape` этому объекту. Поэтому нам нужно использовать традиционное приведение типа `(Shape)`. Это самое простое применение RTTI (Run-Time Type Information), поскольку все преобразования в Java выполняются во время выполнения для обеспечения корректности. Именно это и есть смысл RTTI: тип объекта распознается во время выполнения.
В данном случае RTTI применяется частично: Object
преобразуется в Shape
, но не в Circle
, Square
или Triangle
. Это связано с тем, что мы можем быть уверены только в том, что Vector
содержит геометрические фигуры, но не знаем их конкретных типов. Во время компиляции мы полагаемся на свои собственные правила; во время выполнения же это проверяется через преобразование.Сейчас ситуация контролируется полиморфизмом, и для каждого Shape
вызывается соответствующий метод, чтобы определить, является ли ссылка Circle
, Square
или Triangle
. Общее правило состоит в том, чтобы всегда использовать полиморфический подход. Мы стремимся сделать наш код максимально абстрактным относительно конкретных типов объектов, концентрируясь лишь на общих характеристиках одного типа объектов (в данном случае — Shape
). Только таким образом наш код становится легче реализовать, понять и модифицировать. Таким образом, полиморфизм является обычной целью объектно-ориентированного программирования. Однако, если вы столкнулись с особой проблемой программирования, которую можно решить наиболее легко только после того, как будет точно известен тип обычной ссылки, что делать в этом случае? Например, иногда мы хотим, чтобы наши пользователи могли сделать все конкретные геометрические фигуры (например, треугольники) фиолетового цвета для выделения и быстрого поиска всех фигур этого типа. В этом случае используется технология RTTI (Run-Time Type Information), которая позволяет запросить точный тип ссылки Shape
.
Class
Чтобы понять, как работает RTTI в Java, сначала следует узнать, как информация о типах представляется во время выполнения программы. Для этого используются специальные объекты, называемые «объектами типа Class
», содержащие информацию о классах (иногда их также называют «метаклассами»). В действительности, мы используем объекты типа Class
, чтобы создавать все обычные объекты, принадлежащие определённому классу.Для каждого класса, являющегося частью программы, существует свой объект типа Class
. Другими словами, каждый раз при создании нового класса одновременно создаётся и объект типа Class
(более точно, он хранится в файле с расширением .class
с тем же названием). Во время выполнения программы, когда требуется создать объект данного класса, Java-виртуальная машина (JVM) сначала проверяет, был ли уже загружен объект типа Class
для этого класса. Если нет, JVM ищет файл с расширением .class
с таким же названием и загружает его. Таким образом, при запуске Java-программы она не загружается полностью сразу, что отличает её от многих традиционных языков программирования.
Как только объект типа Class
для данного класса загружается в память, им создаются все объекты этого типа.
Если вы чувствуете некоторое затруднение или недопонимание этой концепции, следующий демонстрационный пример может помочь вам лучше её освоить:```java //: SweetShop.java // Исследование работы загрузчика классов
class Candy { static { System.out.println("Загрузка Candy"); } }
class Gum { static { System.out.println("Загрузка Gum"); } }
class Cookie { static { System.out.println("Загрузка Cookie"); } }
public class SweetShop { public static void main(String[] args) { System.out.println("внутри метода main"); new Candy(); System.out.println("После создания Candy"); try { Class.forName("Gum"); } catch(ClassNotFoundException e) { e.printStackTrace(); } System.out.println( "После Class.forName("Gum")"); new Cookie(); System.out.println("После создания Cookie"); } } ///:~
Для каждого класса (`Candy`, `Gum` и `Cookie`) существует статический блок, который выполняется при первой загрузке класса. Соответствующая информация выводится на экран, указывая на момент загрузки. В методе `main()`, код создания объектов расположен между печатающими сообщениями, чтобы наблюдать за временем загрузки.
Особо интересной строкой является:
```java
Class.forName("Gum");
Метод является статическим членом класса Class
. Объект класса Class
и все остальные объекты аналогичны, поэтому можно получить и контролировать его ссылку (модуль загрузки выполняет эту задачу). Чтобы получить ссылку на класс, одним из способов является использование метода forName()
, который принимает строковое значение с текстовым названием целевого класса (учтите правильность написания и регистр букв). В конце возвращается ссылка на класс.Программа выводит следующее:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
Как видно, каждый класс загружается только тогда, когда это требуется, а статическая инициализация выполняется при загрузке класса.
Очень интересно, что выход другой JVM может выглядеть по-другому:
Loading Candy
Loading Cookie
inside main
After creating Candy
Loading Gum
After Class.forName("Gum")
After creating Cookie
Похоже, что JVM прогнозирует необходимость классов Candy
и Cookie
на основе кода внутри метода main()
, но не видит необходимости загрузки класса Gum
, так как он создается через вызов метода forName()
, а не более типичным образом через ключевое слово new
. Хотя эта JVM также достигает желаемого эффекта, загружая нужные классы до того, как они требуются, поведение здесь может не всегда быть корректным.
(1) Классовые маркеры
В Java 1.1 можно использовать второй подход для получения ссылки на объект класса Class
: использование "классового маркера". Для приведённого выше примера это будет выглядеть следующим образом:
Gum.class;
Это не только проще, но и безопаснее, поскольку проверяется во время компиляции. Удаление необходимости вызова метода делает выполнение программы эффективнее.Классовые маркеры могут применяться не только к обычным классам, но также к интерфейсам, массивам и базовым типам данных. Кроме того, для каждого из базовых типов данных существует стандартное поле с именем TYPE
, которое используется для создания ссылки на объект класса для соответствующего базового типа данных. Например:| ... | эквивалентно ... |
| --- | --- |
| boolean.class
| Boolean.TYPE
|
| char.class
| Character.TYPE
|
| byte.class
| Byte.TYPE
|
| short.class
| Short.TYPE
|
| int.class
| Integer.TYPE
|
| long.class
| Long.TYPE
|
| float.class
| Float.TYPE
|
| double.class
| Double.TYPE
|
| void.class
| Void.TYPE
|
На данный момент нам известны следующие формы RTTI:
(1) Классическое преобразование, такое как (Shape)
, которое использует RTTI для обеспечения корректности преобразования и генерирует исключение ClassCastException
при неудачном преобразовании.
(2) Объект типа Class
, представляющий тип объекта. Этот объект можно запросить для получения полезной информации о времени выполнения.В C++, классическое преобразование (Shape)
не выполняет RTTI. Оно просто сообщает компилятору обрабатывать объект как новый тип. В то время как Java выполняет проверку типа, что обычно называют "типобезопасным" преобразованием вниз. Название "преобразование вниз" происходит от исторической структуры иерархии классов. Преобразование Circle
(круг) в Shape
(геометрическая фигура) называется преобразованием вверх, так как круг является подмножеством всех геометрических фигур. Наоборот, преобразование Shape
в Circle
называется преобразованием вниз. Однако, хотя мы явно знаем, что Circle
также является Shape
, компилятор может автоматически выполнять преобразование вверх, но не гарантирует, что любой Shape
обязательно является Circle
. Поэтому компилятор запрещает автоматическое преобразование вниз, если это не указано явно.RTTI существует в трех формах в Java. Ключевое слово instanceof
позволяет узнать, является ли объект экземпляром определённого типа (Instance
— "экземпляр"). Это возвращает логическое значение, чтобы использовать его в качестве условия, как показано ниже:
if (x instanceof Dog) {
((Dog)x).bark();
}
Перед преобразованием x
в Dog
, условие if
проверяет, принадлежит ли объект x
к классу Dog
. Использование instanceof
особенно важно, когда нет других данных о типе объекта, иначе будет выброшено исключение ClassCastException
.
Наиболее общим подходом является поиск определённого типа (например, треугольника, который должен стать фиолетовым), но программа ниже демонстрирует использование instanceof
для маркировки всех объектов.
//: PetCount.java
// Использование instanceof
package c11.petcount;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
PetCount
public class PetCount {
static String[] типы = {
"Pet", "Собака", "Пудель", "Кошка",
"Грызун", "Хомяк", "Мышь",
};
public static void main(String[] args) {
Vector животные = new Vector();
try {
Class[] типыЖивотных = {
Class.forName("c11.petcount.Собака"),
Class.forName("c11.petcount.Пудель"),
Class.forName("c11.petcount.Кошка"),
Class.forName("c11.petcount.Грызун"),
Class.forName("c11.petcount.Хомяк"),
Class.forName("c11.petcount.Мышь"),
};
for (int i = 0; i < 15; i++)
животные.addElement(
типыЖивотных[(int)(Math.random() * типыЖивотных.length)].newInstance());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {}
Hashtable h = new Hashtable();
for (int i = 0; i < типы.length; i++)
h.put(типы[i], new Counter());
for (int i = 0; i < животные.size(); i++) {
Object o = животные.elementAt(i);
if (o instanceof Pet)
((Counter)h.get("Pet")).i++;
if (o instanceof Собака)
((Counter)h.get("Собака")).i++;
if (o instanceof Пудель)
((Counter)h.get("Пудель")).i++;
if (o instanceof Кошка)
((Counter)h.get("Кошка")).i++;
if (o instanceof Грызун)
((Counter)h.get("Грызун")).i++;
if (o instanceof Хомяк)
((Counter)h.get("Хомяк")).i++;
if (o instanceof Мышь)
((Counter)h.get("Мышь")).i++;
}
for (int i = 0; i < животные.size(); i++)
System.out.println(животные.elementAt(i).getClass().toString());
for (int i = 0; i < типы.length; i++)
System.out.println(типы[i] + " количество: " + ((Counter)h.get(типы[i])).i);
}
}
///:~
В Java 1.0 существует небольшое ограничение для оператора `instanceof`: его можно использовать только для сравнения с уже определённым типом, а не с объектом типа `Class`.
В приведённом примере может показаться, что запись всех этих выражений с использованием instanceof
является довольно трудоёмким процессом. На самом деле это так. Однако в Java 1.0 нет способа автоматизации этой работы — нельзя создать Vector
из объектов типа Class
и сравнивать их.## Класс PetCount2
При наличии большого количества выражений instanceof
, возможно, стоит переоценить дизайн программы.
Конечно, этот пример представляет собой лишь идею — лучше всего добавить статическое поле данных в каждый тип, которое будет увеличиваться при каждом вызове конструктора, чтобы отслеживать количество экземпляров. При программировании вы можете считать, что имеете контроль над исходным кодом класса и возможность его изменения. Но поскольку это не всегда так, RTTI оказывается особенно удобной.
Пример PetCount.java
можно переписать с использованием литералов класса в Java 1.1:
//: PetCount2.java
// Использование литералов класса в Java 1.1
package c11.petcount2;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
``````Здесь массив `typenames` (типовые названия) был удален, и вместо него типы берутся из объекта `Class`. Обратите внимание на дополнительную работу, которую это требует: например, имя класса не `Gerbil`, а `c11.petcount2.Gerbil`, что включает название пакета. Также обратите внимание, что система способна различать классы и интерфейсы.```
```java
public class PetCount2 {
public static void main(String[] args) {
Vector pets = new Vector();
Class[] petTypes = {
// Литералы класса работают в Java 1.1+ только:
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Оффсет на один, чтобы исключить Pet.class:
int rnd = 1 + (int)(Math.random() * (petTypes.length - 1));
pets.addElement(petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
Hashtable h = new Hashtable();
for(int i = 0; i < petTypes.length; i++)
h.put(petTypes[i].toString(), new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.elementAt(i);
if(o instanceof Pet)
((Counter)h.get("class c11.petcount2.Pet")).i++;
if(o instanceof Dog)
((Counter)h.get("class c11.petcount2.Dog")).i++;
if(o instanceof Pug)
((Counter)h.get("class c11.petcount2.Pug")).i++;
if(o instanceof Cat)
((Counter)h.get("class c11.petcount2.Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get("class c11.petcount2.Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get("class c11.petcount2.Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get("class c11.petcount2.Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(pets.elementAt(i).getClass().toString());
Enumeration keys = h.keys();
while(keys.hasMoreElements()) {
String nm = (String)keys.nextElement();
Counter cnt = (Counter)h.get(nm);
System.out.println(nm.substring(nm.lastIndexOf('.') + 1) + " количество: " + cnt.i);
}
}
}
```Также видно, что создание модуля `petTypes` больше не требует окружения блока `try`, так как проверка происходит во время компиляции и не приводит к выбросу исключения, как это было бы с `Class.forName()`.
После динамического создания объекта `Pet` можно заметить, что случайные числа теперь ограничены диапазоном от 1 до `petTypes.length`, причём ноль исключён. Это связано с тем, что ноль представляет собой `Pet.class`, и обычный объект типа `Pet` может не представлять интереса. Однако, поскольку `Pet.class` является частью `petTypes`, все объекты типа `Pet` будут учтены в счёте.
### Динамическое использование оператора `instanceof`
В Java 1.1 метод `isInstance` был добавлен к классу `Class`. Он позволяет динамически использовать оператор `instanceof`. В Java 1.0 этот оператор мог использоваться только статически (как уже упоминалось ранее). Поэтому все эти неприятные строки с `instanceof` могут быть удалены из примера `PetCount`. Пример:
```java
//: PetCount3.java
// Используя метод isInstance() из Java 1.1
package c11.petcount3;
import java.util.*;
class Pet {}
class Dog extends Pet {}
class Pug extends Dog {}
class Cat extends Pet {}
class Rodent extends Pet {}
class Gerbil extends Rodent {}
class Hamster extends Rodent {}
class Counter { int i; }
PetCount3````Учрежденная Java 1.1 методом
isInstance(), больше нет необходимости использовать выражение
instanceof. Кроме того, это также означает, что при добавлении новых типов питомцев достаточно просто изменить массив
petTypes; нет необходимости вносить изменения в остальную часть программы (в отличие от случаев использования
instanceof`).Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )