Java прави своето RTTI чрез обекта Class, даже ако правите нещо от рода на каст. Класът Class също има някои пътища за използване на RTTI.
Първо, трябва да се сдобиете с манипулатор към съответния Class обект. Единият начин за това, както беше показано в предишния пример, е да се използва стринг и Class.forName( ) метода. Това е удобно понеже не е необходим обект от дадения тип за да се получи Class манипулатор. Обаче ако вече има обект от типа от който се интересувате, може да вземете Class манипулатора чрез викане на метод който е част от Object кореновия клас: getClass( ). Това връща Class манипулатор представящ фактическия тип на обекта. Class има някои интересни и понякога полезни методи, демонстрирани в следния пример:
//: c11:ToyTest.java
// Testing class Class
interface HasBatteries {}
interface Waterproof {}
interface ShootsThings {}
class Toy {
// Comment out the following default
// constructor to see
// NoSuchMethodError from (*1*)
Toy() {}
Toy(int i) {}
}
class FancyToy extends Toy
implements HasBatteries,
Waterproof, ShootsThings {
FancyToy() { super(1); }
}
public class ToyTest {
public static void main(String[] args) {
Class c = null;
try {
c = Class.forName("FancyToy");
} catch(ClassNotFoundException e) {}
printInfo(c);
Class[] faces = c.getInterfaces();
for(int i = 0; i < faces.length; i++)
printInfo(faces[i]);
Class cy = c.getSuperclass();
Object o = null;
try {
// Requires default constructor:
o = cy.newInstance(); // (*1*)
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
printInfo(o.getClass());
}
static void printInfo(Class cc) {
System.out.println(
"Class name: " + cc.getName() +
" is interface? [" +
cc.isInterface() + "]");
}
} ///:~
Може да видите че class FancyToy е доста сложен, като наследява от Toy и implements interface-и на HasBatteries, Waterproof и ShootsThings. В main( ) се създава манипулатор на Class и се инициализира на FancyToy Class чрез forName( ) вътре в подходящ try блок.
Методът Class.getInterfaces( ) връща масив от Class обекти представящи интерфейсите които се съдържат в Class обекта от който се интересуваме.
Ако имате Class обект може също да го питате за директния му базов клас чрез getSuperclass( ). Това, разбира се, връща Class манипулатор който може да изследвате по-нататък. Това значи че, по време на изпълнение, може да получите цялата йерархия на класа.
Методът newInstance( ) на Class може, отначало, да изглежда просто друг начин да clone( ) обект. Обаче може да създадете нов обект чрез newInstance( ) без съществуващ обект, както се вижда тук, понеже няма обект Toy, само cy, което е манипулатор към Class обекта на y. Това е начин да се реализира “виртуален конструктор,” който позволява да се каже “Не знам точно какъв тип си, но вземи че се създай правилно.” В примера по-горе, cy е точно Class манипулатор без никаква допълнителна информация известна по време на компилация. И когато създавате нов екземпляр, получавате Object манипулатор. Но този манипулатор сочи Toy обект. Разбира се, преди да може да изпращате съобщения различни от тези за Object, ще трябва да го изследвате малко и да направите някакъв кастинг. Освен това класът кайто се създава с newInstance( ) трябва да има конструктор по подразбиране. Няма начин да се използва newInstance( ) за създаване на обекти без конструктор по подразбиране, така че това може да бъде малко ограничаващо в Java 1. Обаче API на рефлексията в Java 1.1 (обсъждан в следващата секция) позволява динамично да се използва всякакъв конструктор в един клас.
Последния метод в листинга е printInfo( ), който взема Class манипулатор и името с getName( ), после гледа дали е интерфейс с isInterface( ).
Изходът от тази програма е:
Class name: FancyToy is interface? [false]
Class name: HasBatteries is interface? [true]
Class name: Waterproof is interface? [true]
Class name: ShootsThings is interface? [true]
Class name: Toy is interface? [false]
Така, с Class обекта може да намерите всичко което искате да знаете за обект.
Рефлексия: информация за клас по време на изпълнение
Ако не знаете точния тип на обект, RTTI ще ви го каже. Обаче има ограничение: типът трябва да е известен по време на компилация за да може да използвате RTTI и да правите нещо полезно с информацията. С други думи казано, компилаторът трябва да знае всички класове с които работите за RTTI.
Отначало това не изглежда силно ограничение, но да предположим че имате манипулатор който е извън вашето програмно пространство. Фактически класът на този обект е недостъпен за вашата програма даже по време на компилация. Да предположим че сте взели байтове от дисков файл или мрежова връзка и знаете че те представляват клас. Понеже компилаторът не знае за класа когато компилира кода, как би могъл да използува този клас?
В традиционните програмни среди това изглежда като изсмукан от пръсти сценарий. Но ако преминем към по-голям програмен свят виждаме важни случаи когато това се случва. Първият е компонентно-базираното програмиране където строите проекти чрез Rapid Application Development (RAD) в инструмент за производство на приложения. Това е визуалният подход към правене на програми (които виждате на екрана като форми) чрез местене на икони които представят компоненти във формата. Тези компоненти после се конфигурират чрез поставяне на някои стойности по време на компилация. Тази конфигурация по време на проектирането изисква от всеки компонент да може да се прави екземпляр и той да показва част от себе си и да позволява стойностите му да бъдат четени и поставяни. Освен това компонентите които поддържат GUI събитията трябва да показват информация за методите така че RAD средата да може да асистира на програмиста в подтискането на тези методи за обработка на събитията. Рефлексията дава механизъм за откриване на достъпните методи и имената им. Java 1.1 дава начин за компонентно програмиране посредством Java Beans (описани в глава 13).
Друга примамлива мотивация за намиране на информация по време на изпълнение е възможността да се създават и активират обекти отдалечено и много платформено през мрежа. Това е наречено Remote Method Invocation (RMI) и позволява на Java програма (версия 1.1 и по-висока) да има разпределени по много машини обекти. Това разпределяне може да се появи по много причини: може би правите задача с интензивни изчисления и искате да я накъсате и да дадете някои парчета на машини които циклят напразно за да ускорите нещата. В някои ситуации може да искате кода който обработва някои конкретни типове задачи (напр. “Бизнес Правила” в многоредична клиент/сървър архитектура) да е на определена машина така че тя да стане общо хранилище за тези данни та да може да се праменят лесно и да важат за всички потребители. (Това е интересно изследване понеже машина съществува само за да прови промените лесни!) Паралелно с всичко това, разпределената обработка също поддържа специализиран хардуер който може да е добър за отделна задача – обръщане на матрици, например – но неподходящ и твърде скъп за програмни системи с общо предназначение.
В Java 1.1 класът Class (описан по-рано в тазци глава) е разширен за да поддържа концепцията за рефлексия, та има допълнителна библиотека, java.lang.reflect, с класовете Field, Method и Constructor (всеки от които прилага Member interface). Обекти от тези типове се създават от JVM по време на изпълнение за да продставят членовете на непознат клас. После може да използвате Constructorите за създаване на нови обекти, методите get( ) и set( ) за четене и промяна на полетата асоциирани с Field обекти, метод invoke( ) за викане на метод асоцииран с Method обект. Освен това може да викате методите за удобство getFields( ), getMethods( ), getConstructors( )и т.н., за да върнат масиви от обекти представящи полета, методи и конструктори. (Може да намерите повече чрез разглеждане на Class във вашата онлайн документация.) Така информацията за класа за анонимен обект може напълно да се извлече по време на изпълнение, не е необходимо нищо да се знае по време на компилация.
Важно е да се разбере че няма нищо магическо в рефлексията. Когато използвате рефлексията за работа с обект от неизвестен тип JVM просто ще погледне обекта и ще види дали принадлежи към определен клас (точно както обикновения RTTI) но след това, преди да може да направи каквото и да е друго нещо, Class обектът трябва да бъде натоварен. Така .class файлът за конкретния тип трябва все пак да бъде достъпен за JVM, било на локалната машина или през мрежата. Така че истинската разлика между RTTI и рефлексията е че с RTTI компилаторът отваря и разглежда .class файла по време на компилация. С други думи, може да викате всички методи на обект по “нормален” начин. С рефлексията .class файлът е недостъпен по време на компилация; той се отваря и използва от програмната среда по време на изпълнение.
Екстрактор на методите
Рядко ще става нужда да се използват инструментите на рефлексията; те са в езика за поддръжка на други черти на Java като сериализацията на обекти (описана в глава 10), Java Beans и RMI (описано по-късно в тази книга). Има обаче случаи когато възможността динамично да се извлича информация за клас е твърде полезна. Един извънредно полезен инструмент е екстракторът на информация за методите. Както се спомена по-горе, гледането дефиницията на клас в сорса позволява да се видят само дефинираните или подтиснати методи само в тази дефиниция. Но може да има дузини други които са дошли чрез базовите класове. Да се намерят дефинициите им е както досадно, така и отнемащо много време. За щастие рефлексията дава начин да се напише прост инструмент който автоматично ще показва целия интерфейс. Ето как работи той:
//: c11:ShowMethods.java
// Using Java 1.1 reflection to show all the
// methods of a class, even if the methods are
// defined in the base class.
import java.lang.reflect.*;
public class ShowMethods {
static final String usage =
"usage: \n" +
"ShowMethods qualified.class.name\n" +
"To show all methods in class or: \n" +
"ShowMethods qualified.class.name word\n" +
"To search for methods involving 'word'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
if(args.length == 1) {
for (int i = 0; i < m.length; i++)
System.out.println(m[i].toString());
for (int i = 0; i < ctor.length; i++)
System.out.println(ctor[i].toString());
}
else {
for (int i = 0; i < m.length; i++)
if(m[i].toString()
.indexOf(args[1])!= -1)
System.out.println(m[i].toString());
for (int i = 0; i < ctor.length; i++)
if(ctor[i].toString()
.indexOf(args[1])!= -1)
System.out.println(ctor[i].toString());
}
} catch (ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
} ///:~
Методите на Class getMethods( ) и getConstructors( ) връщат масив от Method и Constructor, респективно. Всеки от тези класове има методи за по-нататъшна дисекция на имена, аргументи и връщани стойности на методите, които те представят. Но може също просто да използвате toString( ), както е направено тук, за да се получи String с цялата сигнатура на метода. TОстаналата част от кода е за извличане на информация от командния ред, определяне дали дадена сигнатура совпада с вашия стринг (чрез indexOf( )) и извеждане на резултатите.
Това показва рефлексия в действие, понеже резултатът даван от Class.forName( ) не може да бъде известен по време на компилация и затова цялата информация за сигнатурите бива извличана по време на изпълнение. Ако изследвате вашата онлайн документация за рефлексията, вие ще видите че има достатъчна поддръжка за извикване на метод който е напълно непознат по време на компилация. Още веднъж, това е нещо което едва ли ще искате да правите сами – поддръжката е налице заради Java и така програмната среда може да работи с Java Beans – но е интересно.
Интересен експеримент е да се пусне java ShowMethods ShowMethods. Това дава листинг който включва public конструктор по подразбиране, даже и да сте видели от сорса че не е дефиниран такъв. Конструкторът който виждате е този автоматично конструиран от компилатора. Ако после направите ShowMethods не-public клас (тоест, приятелски), констлукторът по подразбиране вече не се показва в изходния листинг. На синтезирания конструктор по подразбиране автоматично е даден достъп като на класа.
Изходът за ShowMethods е все още малко уморителен. Например има една част произведена чрез извикване на java ShowMethods java.lang.String:
public boolean
java.lang.String.startsWith(java.lang.String,int)
public boolean
java.lang.String.startsWith(java.lang.String)
public boolean
java.lang.String.endsWith(java.lang.String)
Би било даже по-добре квалификатори като java.lang да можеха да се изрязват. Класът StreamTokenizer въведен в предишната глава може да помогне да се реши този проблем:
//: c11:ShowMethodsClean.java
// ShowMethods with the qualifiers stripped
// to make the results easier to read
import java.lang.reflect.*;
import java.io.*;
public class ShowMethodsClean {
static final String usage =
"usage: \n" +
"ShowMethodsClean qualified.class.name\n" +
"To show all methods in class or: \n" +
"ShowMethodsClean qualif.class.name word\n" +
"To search for methods involving 'word'";
public static void main(String[] args) {
if(args.length < 1) {
System.out.println(usage);
System.exit(0);
}
try {
Class c = Class.forName(args[0]);
Method[] m = c.getMethods();
Constructor[] ctor = c.getConstructors();
// Convert to an array of cleaned Strings:
String[] n =
new String[m.length + ctor.length];
for(int i = 0; i < m.length; i++) {
String s = m[i].toString();
n[i] = StripQualifiers.strip(s);
}
for(int i = 0; i < ctor.length; i++) {
String s = ctor[i].toString();
n[i + m.length] =
StripQualifiers.strip(s);
}
if(args.length == 1)
for (int i = 0; i < n.length; i++)
System.out.println(n[i]);
else
for (int i = 0; i < n.length; i++)
if(n[i].indexOf(args[1])!= -1)
System.out.println(n[i]);
} catch (ClassNotFoundException e) {
System.out.println("No such class: " + e);
}
}
}
class StripQualifiers {
private StreamTokenizer st;
public StripQualifiers(String qualified) {
st = new StreamTokenizer(
new StringReader(qualified));
st.ordinaryChar(' '); // Keep the spaces
}
public String getNext() {
String s = null;
try {
if(st.nextToken() !=
StreamTokenizer.TT_EOF) {
switch(st.ttype) {
case StreamTokenizer.TT_EOL:
s = null;
break;
case StreamTokenizer.TT_NUMBER:
s = Double.toString(st.nval);
break;
case StreamTokenizer.TT_WORD:
s = new String(st.sval);
break;
default: // single character in ttype
s = String.valueOf((char)st.ttype);
}
}
} catch(IOException e) {
System.out.println(e);
}
return s;
}
public static String strip(String qualified) {
StripQualifiers sq =
new StripQualifiers(qualified);
String s = "", si;
while((si = sq.getNext()) != null) {
int lastDot = si.lastIndexOf('.');
if(lastDot != -1)
si = si.substring(lastDot + 1);
s += si;
}
return s;
}
} ///:~
Класът ShowMethodsClean е доста приличен на ShowMethods, освен че взема масиви от Method и Constructor и ги превръща в единствен масив от String. Всеки от тези String обекти после се дава на StripQualifiers.Strip( ) за махане на всичката квалификация на метода. Както може да видите, това използва StreamTokenizer и String манипулация за свършване на тази работа.
Този инструмент може реално да спести време докато програмирате, когато се интересувате от конкретен член и не щете да се разхождате в документацията на класовата йерархия, или ако не знаете дали класът може да прави нещо с, да кажем, Color обекти.
Глава 13 съдържа GUI версия на тази програма, така че може да я оставите пусната докато пишете програми, за бърз оглед.
Сподели с приятели: |