Книга е още в много ранна фаза на написване



страница67/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   63   64   65   66   67   68   69   70   ...   73

Резюме


Потоковата библиотека на Java види се удовлетворява основните изисквания: мо­же да се направи четене и писане от/на конзолата, файл, блок памет, даже през Internet (както ще видите в глава 15). Възможно е (чрез наследяване от InputStream и OutputStream) да се създадат нови типове от входни и изходни обек­ти. Може даже да се добави разширяемост към някои видове обекти въз­прие­мани от поток чрез редефиниране на toString( ) метод който автоматично се вика когато подавате обект на метод който очаква String (Ограниччената “авто­матична конверсия на типовете” в Java).

Има въпроси на които не е отговорено с документацията и дизайна на IO по­то­ко­вата библиотека. Например би било приятно да може да се каже че искате да се изхвърли изключение когато се презаписва файл, отворен за извеждане – ня­кои операционни системи позволяват да определите че ще се отваря фойл за пи­сане, но само ако още не съществува. В Java изглежда се очаква да из­полз­ва­те File обек за определяне дали файлът съществува, понеже ако го отворите ка­то FileOutputStream или FileWriter той винаги ще бъде презаписан. Чрез пред­ста­вяне и на пътя, и на файловете класът File също предполага беден дизайн чрез нарушаване на максимата “Не се опитвай да правиш твърде много неща в един клас.”

IO потоковата библиотека докарва смесени чувства. Тя прави повечето от не­ща­та и е преносима. Но ако още не разбирате декораторският подход, дизайнът не е интуитивен, така че има допълнителна работа за ученето и предаването му. Той също не е завършен: няма поддръжка на вида форматиран изход което се под­държа в почти всички други IO пакети на езици. (Това не бе поправено в Java 1.1, който пропусна вазможността да промени дизайна на библиотеката на­пъл­но, а вместо това добави даже повече специални случаи и сложност.) Про­ме­ните на IO библиотеката в Java 1.1 не бяха замествания, а по-скоро добавки, из­глежда, че проектантите на библиотеката не можаха да решат кое ще се изо­ста­вя и кое ще се предпочита, с резултат ядосващо много съобщения за из­полз­ва­не на остарели неща, показващи противоречията в дизайна на библиотеката.

Обаче като веднъж разберете декораторите и започнете да използвате би­блио­те­ката в ситуации, които изискват гъвкавост, може да започнете да печелите от то­зи дизайн, в която точка неговата цена в добавъчни линии може да не ви драз­ни толкова много вече.


Упражнения


  1. Отворете файл така че да може да го четете ред по ред. Четете всеки ред като String и сложете този String обект в ArrayList. Изведете всички редове в ArrayList в обратен ред.

  2. Променете упражнение 1 така че името на файла да се дава като аргумент на ко­мандния ред.

  3. Променете упражнение 2 също да отваря текстов файл така че да може да пишете текст в него. Напишете редовете в ArrayList, заедно с номерата им, във файла.

  4. Променете упражнение 2 да промените всички редове в ArrayList да бъдат само с главни букви и изведете резултата на System.out.

  5. Променете упражнение 2 да вземе допълнителни аргументи от думи които да на­мери във файла. Изведете всички редове в които има думите.

  6. В Blips.java, копирайте файла и го преименувайте на BlipCheck.java и преиме­нувайте класа Blip2 на BlipCheck (правейки го public в това време). Мах­нете всички //! във файла и изпълнете програмата в този вид. После изко­мен­тирайте конструктора по подразбиране на BlipCheck. Пуснете про­гра­ма­та и обяснете защо работи.

  7. В Blip3.java изкоментирайте двата реда след фразата “You must do this:” и пуснете програмата. Обяснете резултата и защо той се различава от този с двете линии.

  8. Превърнете SortedWordCount.java програмата да използва Java 1.1 IO потоци.

  9. Поправете програмата CADState.java както е описано в текста.

  10. (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:

  1. Класическия каст, напр. “(Shape),” който използва RTTI за да осигури ко­рект­ността на каста и изхвърли ClassCastException ако кастът е лош.

  2. Обектът 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 изразите).




Сподели с приятели:
1   ...   63   64   65   66   67   68   69   70   ...   73




©obuch.info 2024
отнасят до администрацията

    Начална страница