1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
11.1 对RTTI的需要.md 29 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

11.1 Нужность RTTI

Рассмотрим следующий пример знакомой структуры классов, использующей полиморфизм. Общий тип — это класс 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.

11.1.1 Объекты типа 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 |

11.1.2 Проверка перед преобразованием

На данный момент нам известны следующие формы 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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master