Резюме
Потоковата библиотека на Java види се удовлетворява основните изисквания: може да се направи четене и писане от/на конзолата, файл, блок памет, даже през Internet (както ще видите в глава 15). Възможно е (чрез наследяване от InputStream и OutputStream) да се създадат нови типове от входни и изходни обекти. Може даже да се добави разширяемост към някои видове обекти възприемани от поток чрез редефиниране на toString( ) метод който автоматично се вика когато подавате обект на метод който очаква String (Ограниччената “автоматична конверсия на типовете” в Java).
Има въпроси на които не е отговорено с документацията и дизайна на IO потоковата библиотека. Например би било приятно да може да се каже че искате да се изхвърли изключение когато се презаписва файл, отворен за извеждане – някои операционни системи позволяват да определите че ще се отваря фойл за писане, но само ако още не съществува. В Java изглежда се очаква да използвате File обек за определяне дали файлът съществува, понеже ако го отворите като FileOutputStream или FileWriter той винаги ще бъде презаписан. Чрез представяне и на пътя, и на файловете класът File също предполага беден дизайн чрез нарушаване на максимата “Не се опитвай да правиш твърде много неща в един клас.”
IO потоковата библиотека докарва смесени чувства. Тя прави повечето от нещата и е преносима. Но ако още не разбирате декораторският подход, дизайнът не е интуитивен, така че има допълнителна работа за ученето и предаването му. Той също не е завършен: няма поддръжка на вида форматиран изход което се поддържа в почти всички други IO пакети на езици. (Това не бе поправено в Java 1.1, който пропусна вазможността да промени дизайна на библиотеката напълно, а вместо това добави даже повече специални случаи и сложност.) Промените на IO библиотеката в Java 1.1 не бяха замествания, а по-скоро добавки, изглежда, че проектантите на библиотеката не можаха да решат кое ще се изоставя и кое ще се предпочита, с резултат ядосващо много съобщения за използване на остарели неща, показващи противоречията в дизайна на библиотеката.
Обаче като веднъж разберете декораторите и започнете да използвате библиотеката в ситуации, които изискват гъвкавост, може да започнете да печелите от този дизайн, в която точка неговата цена в добавъчни линии може да не ви дразни толкова много вече.
-
Отворете файл така че да може да го четете ред по ред. Четете всеки ред като String и сложете този String обект в ArrayList. Изведете всички редове в ArrayList в обратен ред.
-
Променете упражнение 1 така че името на файла да се дава като аргумент на командния ред.
-
Променете упражнение 2 също да отваря текстов файл така че да може да пишете текст в него. Напишете редовете в ArrayList, заедно с номерата им, във файла.
-
Променете упражнение 2 да промените всички редове в ArrayList да бъдат само с главни букви и изведете резултата на System.out.
-
Променете упражнение 2 да вземе допълнителни аргументи от думи които да намери във файла. Изведете всички редове в които има думите.
-
В Blips.java, копирайте файла и го преименувайте на BlipCheck.java и преименувайте класа Blip2 на BlipCheck (правейки го public в това време). Махнете всички //! във файла и изпълнете програмата в този вид. После изкоментирайте конструктора по подразбиране на BlipCheck. Пуснете програмата и обяснете защо работи.
-
В Blip3.java изкоментирайте двата реда след фразата “You must do this:” и пуснете програмата. Обяснете резултата и защо той се различава от този с двете линии.
-
Превърнете SortedWordCount.java програмата да използва Java 1.1 IO потоци.
-
Поправете програмата CADState.java както е описано в текста.
-
(Intermediate) В глава 7, намерете GreenhouseControls.java примера, който се състои от три файла. В GreenhouseControls.java вътрешният клас Restart( ) има твърдо вградена система от събития. Променете програмата така, че да чете събитията и техните относителни времена от текстов файл. (Challenging: Използвайте factory метод от глава 16 за постройка на събитията.)
11: Идентификация на типа по време на изпълнение
Идеята за това (RTTI) изглежда доста проста отначало: дава се възможност да се намери точния тип на обекта имайки само манипулатор към базовия тип.
Обаче, нуждата от RTTI разкрива лабиринт от интересни (и често объркващи) въпроси на ОО дизайн и поставя фундаментални въпроси относно структурирането на програмите.
Тази глава поглежда към начините по които Java подволява да се открие информация за обектите и класовете по време на изпълнение. Има две форми: “традиционно” RTTI, което предполага че всички типове са налични по време на компилация и по време на изпълнение и “reflection” механизма в Java 1.1, който позволява да се открие информация единствено по време на изпълнение. “Традиционното” RTTI ще се разгледа първо, следвано от дискусия за рефлексията.
Нуждата от RTTI
Да видим познатия сега пример с йерархия. Родов е базовият клас Shape, а специфичните извлечени класове са Circle, Square и Triangle:
…
Това е типична диаграма на йерархия на класове, с базовия клас най-отгоре и разрастваща се надолу с извлечените класове. Нормалната цел на ОО програмирането е да може вашият код да манипулира манипулаторите на базовия тип (Shape, в този случай), така че ако решите да разширите програмата с нов клас (Rhomboid, извлечен от Shape, например), кодът да не се засяга. В този пример динамично свързван метод в интерфейса на Shape е draw( ), така че намерението е клиент-програмистът да вика draw( ) чрез родов Shape манипулатор. draw( ) е подтиснат във всичките извлечени класове, а понеже е динамично свързан метод, правилното поведение ще е налице даже и да се вика чрез родовия Shape манипулатор. Това е полиморфизъм.
Изобщо, създавате специфичен обект (Circle, Square или Triangle), ъпкаствате го към Shape (забравяйки специфичния тип на обекта) и използвате този анонимен Shape манипулатор в останалата част на програмата.
Като кратък преглед на полиморфизма и ъпкастинга, бихте могли да кодирате горното така: (Вижте стр 89 ако имате проблеми с пускането на тази програма.)
//: c11:Shapes.java
package c11;
import java.util.*;
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Circle.draw()");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Square.draw()");
}
}
class Triangle implements Shape {
public void draw() {
System.out.println("Triangle.draw()");
}
}
public class Shapes {
public static void main(String[] args) {
ArrayList s = new ArrayList();
s.add(new Circle());
s.add(new Square());
s.add(new Triangle());
Iterator e = s.iterator();
while(e.hasNext())
((Shape)e.next()).draw();
}
} ///:~
Базовият клас би могъл да бъде кодиран като interface, abstract клас, или обикновен клас. Понеже Shape няма конкретни членове (тоест такива с дефиниции) и не се очаква някога да създадете конкретен Shape обект, най-подходящото и гъвкаво представяне е interface. То също е и по-чисто защото нямате многобройните abstract ключови думи да се моткат наоколо.
Всеки от извлечените класове подтиска метода на базовия клас draw така че да се получи различно поведение. В main( ), специфичните типове на Shape се създават и добавят към ArrayList. Това е мястото където става ъпкастът понеже ArrayListъът държи само Objectи. Понеже всичко в Java (с изключение на примитивите) е Object, ArrayList може също да държи Shape обекти. Но поради ъпкастът към Object, губи се всякаква специфична информация, включително фактът че обектите са shapeове. За ArrayListа те са си Objectи.
В точката в която вземате елемент от ArrayList с next( ), нещата стават малко сгъстени. Понеже ArrayList държи само Objectи, next( ) естествено дава манипулатор към Object. Но ние знаем че той в действително е манипулатор на Shape и искаме да пратим Shape съобщения към него обект. Така че е необходим каст към Shape чрез традиционния начин “(Shape)”. Това е най-основната форма на RTTI, понеже в Java всички кастове се проверяват по време на изпълнение за коректност. Това е точно каквото значи RTTI: по време на изпълнение се намира типа на обекта.
В този случай кастът на RTTI е само частичен: Object е каст към Shape, а не по целия път до Circle, Square или Triangle. Това е защото единственото нещо което знаем в тази точка е че ArrayList е пълен с Shapeове. По време на компилация това е наложено по ваши собствени правила, но по време на изпълнение кастът го осигурява.
Сега се намесва полиморфизмът и точният метод за Shape се определя по това дали конкретния манипулатор е за Circle, Square или Triangle. И изобщо, това е както трябва да бъде; вие искате вашия код да знае колкото е възможно по-малко от спецификата на типовете на обектите и само да се оправя с общото представяне на фамилия от обекти (в този случай, Shape). Като резултат вашият код ще е по-лесен за четене, писане и поддръжка, дизайнът — по-лесен за реализация, разбиране и промяна. Така че полиморфизмът е обща цел в ООП.
Ами ако имате да решавате специфичен проблем, което по-лесно може да стане със знаенето на конкретния тип на манипулатора? Например да предположим че искате да позволите на вашите потребители да засветляват всичките фигури от конкретен тип чрез провенето им морави. По този начин те биха могли да открият всичките триъгълници на екрана засветлявайки ги. Това е което свършва RTTI: може да питате за каъв точно тип се отнася даден манипулатор на Shape.
Обектът Class
За да се разбере как RTTI работи в Java първо трябва да се знае как информацията се представя по време на изпълнение. Това става чрез специален обект наречен обект Class, който съдържа информация за класа. (Това понякога се нарича мета-клас.) Фактически, обектът Class се използва за създаване на всички “нормални” обекти от вашия клас.
За всеки клас който във вашата програма има обект Class. Тоест всеки път когато пишете нов клас се създава единствен обект Class (и се запомня, съвсем правилно, в идентично наименован .class файл). По време на изпълнение, когато искате да направите обект от този клас, Java Virtual Machine (JVM) първо проверява дали обект Class за въпросния тип е натоварен. Ако не, JVM го товари (в паметта-б.пр.) от .class файл със същото име. Така една Java програма не е напълно натоварена преди да започне, което е различно от всички традиционни езици.
Щом веднъж Class обектът за конкретния тип е в паметта, той се използва за създаване на всички обекти от този тип.
Ако това изглежда мъгливо или не му вярвате, ето демонстрационна програма за доказателство:
//: c11:SweetShop.java
// Examination of the way the class loader works
class Candy {
static {
System.out.println("Loading Candy");
}
}
class Gum {
static {
System.out.println("Loading Gum");
}
}
class Cookie {
static {
System.out.println("Loading Cookie");
}
}
public class SweetShop {
public static void main(String[] args) {
System.out.println("inside main");
new Candy();
System.out.println("After creating Candy");
try {
Class.forName("Gum");
} catch(ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(
"After Class.forName(\"Gum\")");
new Cookie();
System.out.println("After creating Cookie");
}
} ///:~
Всеки от класовете Candy, Gum и Cookie има static клауза която се изпълнява когато класът се зареди (в паметта-б.пр.) за пръв път. Извежда се съобщение че се товари съответния клас. В main( ) създаванията на обекти са обкръжени с оператори за извеждане за да се проследи създаването.
Един особено интересен ред е:
Class.forName("Gum");
Този метод е static член на Class (към който принадлежат всички обекти от Class). Class обектът е като всеки друг и затова може да се вземе манипулатор към него и да се прави нещо с него. (Така прави и товарачът (в паметта - лоудър - б.пр.).) Един от начините да се вземе манипулатор към Class обект е forName( ), който взема String съдържащ името (да се внимава с правописа и капитализацията!) на конкретен клас към който искате манипулатор. Той връща Class манипулатор.
Изходът от тази рограма за една JVM е:
inside main
Loading Candy
After creating Candy
Loading Gum
After Class.forName("Gum")
Loading Cookie
After creating Cookie
Може да видите че всеки обект Class се товари само когато е необходим и че инициализацията на static се изпълнява при товаренето на класа.
Доста интересно, друга 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 дава желаните ефекти понеже товари класовете преди да има нужда от тях, не е яснсигурно дали такова поведение е коректно.
Класни литерали
В Java 1.1 има втори начин за произвеждане на манипулатор към обект Class: чрез класен литерал. В горната програма това би изглеждало така:
Gum.class;
Което не само е по-просто, но също и по-безопасно понеже се проверява по време на компилация. Понеже елиминира извикването на метод, така също е и по-ефективно.
Класният литерал работи както с обикновени класове, така и с интерфейси, масиви, примитиви. В добавка, има стандартно поле TYPE което съществува за всички обгръщащи примитиви класове. Полето TYPE дава манипулатор към Class обект за асоциирания примитивен тип, както тук:
-
… е еквивалентно на …
|
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:
-
Класическия каст, напр. “(Shape),” който използва RTTI за да осигури коректността на каста и изхвърли ClassCastException ако кастът е лош.
-
Обектът Class представящ типа на вашия обект. Обектът Class може да даде полезна информация по време на изпълнение.
В C++ класическият каст “(Shape)” не правят RTTI. Той просто казва на компилатора да третира указателя като на обект от посочения тип. В Java, където се прави проверка на типовете, този каст се нарича често “безопасен даункаст на типа.” Причината за термина “downcast” е историческа: представянето на диаграмата. Ако се превръща Circle в Shape е ъпкаст, тогава от Shape към Circle ще е даункаст. Обаче вие знаете че Circle също е Shape, а компилаторът свободно разрешава ъпкаст, но не знаете дали Shape е непременно Circle, така че компилаторът не прави такъв каст докато не го напишете явно.
Има и трета форма на RTTI в Java. Това е ключовата дума instanceof която казва дали обектът е екземпляр от някакъв конкретен тип. Тя връща boolean така че се използва като въпрос:
if(x instanceof Dog)
((Dog)x).bark();
Горния if оператор проверява дали x принадлежи на класа Dog преди превръщането на x към Dog. Важно е да се използва instanceof преди даункаст, когато няма друга информация за типа; иначе ще се случи ClassCastException.
Обикновено ще ловувате за един тип (триъгълници за да се направят морави, например), но следната програма показва как да се покрият всички обекти чрез instanceof.
//: c11:petcount:PetCount.java
// Using 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; }
public class PetCount {
static String[] typenames = {
"Pet", "Dog", "Pug", "Cat",
"Rodent", "Gerbil", "Hamster",
};
public static void main(String[] args) {
ArrayList pets = new ArrayList();
try {
Class[] petTypes = {
Class.forName("c11.petcount.Dog"),
Class.forName("c11.petcount.Pug"),
Class.forName("c11.petcount.Cat"),
Class.forName("c11.petcount.Rodent"),
Class.forName("c11.petcount.Gerbil"),
Class.forName("c11.petcount.Hamster"),
};
for(int i = 0; i < 15; i++)
pets.add(
petTypes[
(int)(Math.random()*petTypes.length)]
.newInstance());
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
catch(ClassNotFoundException e) {}
HashMap h = new HashMap();
for(int i = 0; i < typenames.length; i++)
h.put(typenames[i], new Counter());
for(int i = 0; i < pets.size(); i++) {
Object o = pets.get(i);
if(o instanceof Pet)
((Counter)h.get("Pet")).i++;
if(o instanceof Dog)
((Counter)h.get("Dog")).i++;
if(o instanceof Pug)
((Counter)h.get("Pug")).i++;
if(o instanceof Cat)
((Counter)h.get("Cat")).i++;
if(o instanceof Rodent)
((Counter)h.get("Rodent")).i++;
if(o instanceof Gerbil)
((Counter)h.get("Gerbil")).i++;
if(o instanceof Hamster)
((Counter)h.get("Hamster")).i++;
}
for(int i = 0; i < pets.size(); i++)
System.out.println(
pets.get(i).getClass().toString());
for(int i = 0; i < typenames.length; i++)
System.out.println(
typenames[i] + " quantity: " +
((Counter)h.get(typenames[i])).i);
}
} ///:~
Има малко тясно ограничение върху instanceof в Java 1.0: Може да го сравнявате с именуван тип само, а не с Class обект. В примера по-горе е ясно че би било досадно да се пишат всички тези instanceof изрази. Но в Java 1.0 няма начин умно да се автоматизира това чрез ArrayList от Class обекти и сравняване. Това не е така голямо ограничение, както може да се помисли, понеже накрая ще се бламира проектът, ако трябва да напишете всички тези instanceof изрази.
Разбира се този пример е нарочен – вие вероятно бихте сложили static даннов член във всеки тип и инкрементирайки го в конструктора ще се пази информация за броя. Бихте правили така ако имахте сорса и можехте да го променяте. Тъй като това не е обикновения случай, RTTI може да дойде твърде на място.
Използване на класни литерали
It’s interesting to see how the PetCount.java example can be rewritten using Java 1.1 class literals. The result is cleaner in many ways:
//: c11:petcount2:PetCount2.java
// Using Java 1.1 class literals
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; }
public class PetCount2 {
public static void main(String[] args) {
ArrayList pets = new ArrayList();
Class[] petTypes = {
// Class literals work in Java 1.1+ only:
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Offset by one to eliminate Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.add(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
HashMap h = new HashMap();
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.get(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.get(i).getClass().toString());
Iterator keys = h.keySet().iterator();
while(keys.hasNext()) {
String nm = (String)keys.next();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" quantity: " + cnt.i);
}
}
} ///:~
Масивът typenames е махнат понеже имената ще се вземат като стрингове от обект Class. Забележете допълнителната работа за това: името на класа не е, например, Gerbil, а е c11.petcount2.Gerbil понеже се включва името на пакета. Забележете също че системата може да прави разлика между класове и интерфейси.
Може също да видите че създаването на petTypes не е необходимо да бъде в try блок понеже се изчислява по време на компилация и няма да изхвърли изключение, за разлика от Class.forName( ).
Когато Pet са създадени динамично, може да видите че генераторът на случайни числа е ограничен между 1 и petTypes.length и не включва нула. Така е защото нулата се отнася за Pet.class и по предположение родовия Pet обект не е интересен. Понеже Pet.class е част от petTypes резултатът е че висчките "петс" се броят.
Динамично instanceof
Java 1.1 е добавил isInstance метод в класа Class. Това позволява динамично да се вика instanceof оператора, което може да се прави само статично в Java 1.0 (както беше показано). Така всички онези досадни instanceof оператори могат да се махнат в PetCount примера:
//: c11:petcount3:PetCount3.java
// Using Java 1.1 isInstance()
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; }
public class PetCount3 {
public static void main(String[] args) {
ArrayList pets = new ArrayList();
Class[] petTypes = {
Pet.class,
Dog.class,
Pug.class,
Cat.class,
Rodent.class,
Gerbil.class,
Hamster.class,
};
try {
for(int i = 0; i < 15; i++) {
// Offset by one to eliminate Pet.class:
int rnd = 1 + (int)(
Math.random() * (petTypes.length - 1));
pets.add(
petTypes[rnd].newInstance());
}
} catch(InstantiationException e) {}
catch(IllegalAccessException e) {}
HashMap h = new HashMap();
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.get(i);
// Using isInstance to eliminate individual
// instanceof expressions:
for (int j = 0; j < petTypes.length; ++j)
if (petTypes[j].isInstance(o)) {
String key = petTypes[j].toString();
((Counter)h.get(key)).i++;
}
}
for(int i = 0; i < pets.size(); i++)
System.out.println(
pets.get(i).getClass().toString());
Iterator keys = h.keySet().iterator();
while(keys.hasNext()) {
String nm = (String)keys.next();
Counter cnt = (Counter)h.get(nm);
System.out.println(
nm.substring(nm.lastIndexOf('.') + 1) +
" quantity: " + cnt.i);
}
}
} ///:~
Може да видите че методът isInstance( ) в Java 1.1 е елиминирал нуждата от instanceof изрази. Още това значи че може да добавяте нови типове "петс" просто чрез промяна на petTypes масива; Останалата част от програмата не иска промени (каквито бяха необходими с instanceof изразите).
Сподели с приятели: |