Философия Java3
Шрифт:
Таким образом, обычно вы создаете объект конкретного класса (Circle, Square или Triangle), проводите восходящее преобразование к фигуре Shape («забывая» точный тип объекта) и используете ссылку на обобщенную фигуру. Реализация иерархии Shape может выглядеть примерно так:
//: typeinfo/Shapes java import java.util *;
abstract class Shape {
void drawO { System out. pri ntl n( this + ".drawO"), } abstract public String toStringO;
}
class Circle extends Shape {
public String toStringO { return "Circle"; }
}
class Square extends Shape {
public String toStringO { return "Square", }
}
class Triangle extends Shape {
public String toStringO { return "Triangle", }
}
public class Shapes {
public static void main(String[] args) {
List<Shape> shapeList = Arrays asList(
new CircleO, new SquareO, new TriangleO
):
for(Shape shape . shapeList) shape. drawO;
}
} /* Output: Circle. drawO Square. drawO Triangle. drawO *///:-
Метод draw
В данном примере восходящее преобразование происходит во время помещения объекта-фигуры в контейнер List<Shape>. В процессе восходящего преобразования теряется конкретная информация, в том числе и точный тип фигур. Контейнеру все равно — он хранит просто объекты Shape.
Когда вы извлекаете из контейнера очередной элемент, контейнер, в котором все элементы хранятся в виде Object, автоматически преобразует результат обратно к Shape. Это наиболее основная форма RTTI, поскольку все подобные преобразования в языке Java проверяются на правильность на стадии исполнения. Именно для этого и служит RTTI: во время выполнения программы проверяется истинный тип объекта.
В нашем случае определение типа происходит частично: тип Object преобразуется к базовому типу фигур Shape, а не к конкретным типам Circle, Square или Triangle. Просто потому, что в данный момент нам известно только то, что контейнер List<Shape> заполнен фигурами Shape. Во время компиляции соблюдение этого требования обеспечивается контейнером и системой параметризованных типов Java, а при выполнении оно подтверждается успешным преобразованием типов.
Теперь в действие вступает полиморфизм — для каждой фигуры Shape вызывается свой метод draw, в зависимости от того, окружность это (Circle), прямоугольник (Square) или треугольник (Triangle). И в основном именно так и должно быть; основная часть кода не должна зависеть от точного типа объекта, она оперирует с универсальным представлением целого семейства объектов (в нашем случае это фигура (Shape)). Такой подход упрощает написание программы, а впоследствии ее чтение и сопровождение. По этой причине полиморфизм часто используется при написании объектно-ориентированных программ.
Но что, если у вас имеется не совсем обычная задача, для успешного решения которой необходимо узнать точный тип объекта, располагая только ссылкой на базовый тип? Допустим, пользователи программы с фигурами хотят выделить определенные фигуры (скажем, все треугольники)
Объект Class
Чтобы понять, как работает RTTI в Java, необходимо знать, каким образом хранится информация о типе во время выполнения программы. Для этой цели используется специальный объект типа Class, который и содержит описание класса. Объект Class используется при создании всех «обыкновенных» объектов любой программы.
Каждый класс, задействованный в программе, представлен своим объектом Class. Иначе говоря, при написании и последующей компиляции нового класса для него создается объект Class (который затем сохраняется в одноименном файле с расширением .class). Для создания объекта этого класса виртуальная машина Java QVM), исполняющая программу, использует подсистему, называемую загрузчиком классов.
Подсистема загрузчиков классов в действительности состоит из цепочки загрузчиков классов, но только основной загрузчик является частью реализации JVM. Основной загрузчик классов загружает так называемые доверенные классы, в том числе классы Java API, с локального диска. Обычно включать дополнительные загрузчики классов в цепочку не требуется, но в особых ситуациях (например, при загрузке классов для поддержки приложений веб-сервера или при загрузке классов по сети) существует возможность подключения дополнительных загрузчиков.
Все классы загружаются в JVM динамически, при первом использовании класса. Таким образом, программа на Java никогда не бывает полностью загружена до начала своего выполнения, и в этом отношении Java отличается от многих традиционных языков. Динамическая загрузка открывает возможности, недоступные или трудно реализуемые в языках со статической загрузкой вроде С++.
Сначала JVM проверяет, загружен ли объект Class для этого нового класса. При отрицательном результате JVM ищет подходящий файл .class и подгружает его (а дополнительный загрузчик, например, может подгружать байт-код из базы данных). Загруженный код класса проходит проверку на целостность и на отсутствие некорректного байт-кода Java (одна из «линий защиты» в Java).
После того как объект Class для определенного типа будет помещен в память, в дальнейшем он используется при создании всех объектов этого типа. Следующая программа проясняет сказанное:
//• typei nfo/SweetShop.java
// Проверка процесса загрузки классов.
import static net.mindview util.Print *.
class Candy {
static { print("3arpy3Ka класса Candy"), }
}
class Gum {
static { print("Загрузка класса Gum"), }
}
class Cookie {
static { System out println("3arpy3Ka класса Cookie"), }
}
public class SweetShop {
public static void main(String[] args) { printC'B методе mainO"), new CandyO;
print("После создания объекта Candy");
try { продолжение &
Class forName("Gum"). } catchCClassNotFoundException e) {
pri nt("Класс Gum не найден").
}
print( "После вызова метода Class forName(\"Gum\")"), new CookieO.
print("После создания объекта Cookie").
}
} /* Output в методе mainO Загрузка класса Candy После создания объекта Candy Загрузка класса Gum