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



страница28/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   24   25   26   27   28   29   30   31   ...   73

Резюме


Изглеждащият сложен механизъм за инициализация, конструкторът, ви дава важ­но доказателство за важността на въпросите на инициализацията. Когато Stroustrup проектирал C++ едно от първите наблюдения които направил за произ­водителността на C било че неправилната инициализация води до зна­чи­тел­на част от програмистките проблеми. Тези видове грешки са трудни за от­кри­ване и подобно е палажението с изчистването. Понеже конструкторите поз­во­ляват да се гарантира правилна инициализация и изчистване (компилаторът ня­ма да позволи създаването на обект без правилни викания на конструктор), полу­чавате пълен контрол и безопасност.

В C++разрушаването е важно, панеже обектите създадени с new трябва да бъ­дат явно разрушавани. В Java боклучарят освобождава паметта на обектите, за­то­ва подобен механизъм не е много необходим. В случаите когато не се нуж­дае­те от деструктороподобно поведение боклучарят на Java много опростява про­грамирането и добавя много необходима безопасност в управлението на памет­та. Някои боклучари даже чистят други ресурси като графики и файлови ма­нипулатори. Обаче боклучарят си има цена, която е трудно да се сложи в перс­пектива поради общата бавност на Java интерпретаторите към момента на на­писването на книгата. Щом това се промени ще можем да преценим дали це­на­та на боклукосъбирането ще предотврати използването на Java за някои ти­по­ве програми. (Един от проблемите е непредсказуемостта на боклучаря.)

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

Упражнения


  1. Създайте клас с конструктор по подразбиране (такъв без аргументи) който из­вежда съобщение. Създайте обект от този клас.

  2. Създайте претоварен конструктор за упражнение 1 който взима String ар­гу­мент и го извежда освен вашето съобщение.

  3. Създайте масив от манипулатори на обектите за класа от упражнение 2, но не създавайте обекти за запълване на масива. Когато пуснете програмата, вижте дали се извеждат съобщения за инициализация.

  4. Завършете упражнение 3 със създаване на обекти за свързване с масива от манипулатори.

  5. Експериментирайте с Garbage.java чрез използване на аргументи “before,” “after” and “none.” Повторете процеса и вижте дали има нещо характерно в из­хода. Променете кода така, че System.runFinalization( ) да се вика преди System.gc( ) и наблюдавайте резултата.


5: Скриване на реализацията


Основно съображение в ООП е “разделяне ня нещата, които се променят от нещата, които остават същите.”

Това в частност е важно за библиотеките. Потребителят (клиент-про­гра­ми­стът) на библиотеката трябва да може да разчита на това, което из­полз­ва и да знае, че няма да има нужда да пренаписва кода си, ако излезе нова вер­сия на библиотеката. От другата страна, създателят на библиотеката трябва да има възможност да подобрява кода си, без това да засяга кона на клиент-про­грамистите.

Това може да се постигне чрез съглашение. Например програмистът на би­блио­те­ката трябва да се съгласи да не премахва методи, понеже това ще наруши ра­бо­тата на клиентската програма. Обратната ситуация е деликатна, обаче. В слу­чая на член-променливи откъде да знае програмистът на класа кои от тях из­полз­ва клиентската програма? Това също се отнася и за методите които са част от реализацията на класа и не са предназначени за директно използване от про­гра­мист. Ами ако създателят на библиотеката иска да премахне стара реа­ли­за­ция и да направи нова? Пробяната на който и да е от членоете би могла да раз­ва­­ли работата на приложната програма. Така създателят на библиотеката е в ус­ми­рителна риза и не може да променя нищо.

За решаването на този проблем Java има спецификатори на достъпа за да се поз­воли на създателят на библиотеката да каже кое е достъпно за клиент-про­гра­­­миста и кое не е. Нивата на достъп от “най-голям достъп” до “най-малък до­стъп” са public, “приятелско” (за него няма ключова дума), protected и private. От предишния параграф може да сте останали с впечатление че, като проектант на библиотеката, ще искате да даржите всичко “private” доколкото е възможно и да показвате само нещата които искате клиент-програмистът да използва. То­ва е точно така, макар и обикновено да противоречи на интуицията на про­гра­ми­сти с други езици (especially C) които са свикнали да имат достъп до всичко без ограничение. В края на тази глава ще бъдете убедени, че е ценен контролът на достъпа в Java.

Концепцията за библиотека от компоненти и кой има достъп до тях не е за­вър­ше­на, обаче. Още остава въпросът как тези компоненти ще се свържат в единна би­блиотечна единица. Това се управлява с ключовата дума package в Java и спе­цификаторите на достъпа се засягат от това дали класът е в същата би­блио­те­ка или в друга. Така че за начало на тази глава ще научите как компонентите се слагат в библиотека. След това ще може да разберете пълното значение на спе­цификаторите на достъпа.

package: библиотечната единица


Това, което се взима когато се използва ключовата дума import за цяла библиотека е пакетът, както в

import java.util.*;

Това взима цялата библиотека ютилита която е част от дистрибутивния ма­те­ри­ал на Java. Тъй като ArrayList е в java.util, може или да посочите цялото име java.util.ArrayList (което може да направите без ключовата дума import), или про­сто да напишете ArrayListimport).

Ако искате само един клас, може да го споменете с import

import java.util.ArrayList;

Сега може да ползвате ArrayList без квалификация. Но никой друг клас от java.util не е достъпен.

Причината за цялото това импортиране е да може да се управляват “про­стран­ства­та на имената.” Имената на всички ваши членове са изолирани едно от дру­го. Метод f( ) вътре в клас A няма да се бърка с f( ) който има същата сигнатура (спи­сък аргументи) в клас B. Ами за имената на класовете? Да допуснем че съз­да­вате stack клас който се инсталира на машина където има вече stack написан от някой друг? С Java в Мрежата това маже да стане понеже потребителят не знае какви класове ще се свалят при употребата на Java програмата.

Поради този потенциален конфликт на имената е важно да се управляват те в Java и да може да се конструират уникални имена независимо от ограниченията на Мрежата.

До тук повечето от примерите се поместваха в един файл и бяха проектирани за локална употреба и нямаше притеснения от имената на пакетите. (В този слу­чай името на класа се слага в “пакета по подразбиране.”) Това е по избор и този под­ход ще се използва на повечето места в книгата. Ако планирате да на­пра­ви­те програма, която е “Internet friendly,” обаче, трябва да мислите за пре­дот­вра­тя­ване на конфликт на имената.

Сорсът на Java обикновено се нарича компилационна единица (понякога тран­сла­ционна единица). Всяка компилационна единица трябва да има име за­върш­ва­що с .java и вътре в нея може да има публичен клас който трябва да има съ­що­то име (включително капитализацията, но без .java разширението на фай­ло­во­то име). Ако не направите така компилаторът ще се оплаква. Може да има са­мо един public във всяка компилационна единица (иначе компилаторът пак ще се оплаква). Останалите класове в единицата, ако ги има, са скрити от света из­вън пакета понеже не са public, те включват “поддържащите” класове за глав­ния public клас.

Когато се компилира .java файл се получава изходен файл с точно същото име но с разширение .class за всеки клас в .java файла. Така може да се получат ня­кол­ко .class файла от всеки .java файл. Ако сте програмирали на компилируем език може да сте свикнали компилаторът да изплюва някаква междинна форма (обикновено “obj” файл) която после се опакова с други от същия род чрез лин­ке­ра (за да се създаде изпълнимия файл) или библиотекаря (за библиотека). То­ва не е начинът на работа на Java. Изпълнимата програма е куп .class файлове, кои­то може да бъдат компресирани и пакетирани в JAR файл (чрез jar юти­ли­ти­то в Java 1.1). Java интерпретаторът е отговорен за намирането, свързването и то­варенето на тези файлове.1

Библиотеката е също куп такива файлове. Всеки има един клас public (не е за­дъл­жително да има public клас, но е типично), така че има един компонент във все­ки файл. Ако искате да кажете че всички тези файлове вървят заедно (които са в свои отделни .java и .class файлове) това е мястото, където ключовата дума package влиза в ролята си.

Когато напишем:

package mypackage;

в началото на файл, където package операторът трябва да е първият не­ко­мен­та­рен ред във файла, вие казвате, че тази компилационна единица е част от би­блио­тека наречена mypackage. Или, с други думи, казвате че public клас името в единицата е под чадъра на mypackage, и ако някой иска да използва името или трябва да го специфицира напълно или да употреби ключовата дума import ком­бинирана с mypackage (използвайки избраните преди неща). Забележете че кон­венцията за Java пакетите е да се използват навсякъде малки букви, даже и за междинните думи.

Например да предположим че името на класа е MyClass.java. Това значи че мо­же да има един и само един public клас в този файли че името на класа може да бъ­де само MyClass (капитализацията е същата):

package mypackage;

public class MyClass {


// . . .

Сега ако някой иска да използва MyClass или, по подобен начин, който и да е от public класовете в mypackage, той трябва да използва ключовата дума import за да направи името или имената в mypackage достъпни. Алтернативата е да се даде пълно име за всеки клас:

mypackage.MyClass m = new mypackage.MyClass();

Ключовата дума import може да го направи много по-просто:

import mypackage.*;
// . . .

MyClass m = new MyClass();

Заслужава си да се помни, че ключовите думи package и import позволяват на про­ектанта на библиотеката да си отдели специално пространство на имената, без значение колко души са в Internet и пишат класове на Java.

Създаване на уникални имена на пакети


Бихте могли да забележите че, понеже пакетът никога не се реално “пакетира” в единствен файл, той може да се състои от много .class файлове и става малка бър­котия. За да се предотврати тя логично е да се кложат всички .class файлове на отделен пакет в отделна директория; тоест да се използва йерархичната струк­тура на операционната система във ваша полза. Така Java се оправя с про­блема с бъркотията.

Това също решава и други два проблема: създаване на уникални имена на па­ке­ти и изравяне на класове, които могат да са забутани някъде в директорната струк­тура. Това се прави, както беше казано в глава 2, чрез кодирането на пътя до .class файла в името на package. Компилаторът изисква това, но по уговорка пър­вата част от името на package е Internet домейновото име на съз­да­теля на класа, обърнато. Тъй като домейновите имена в Internet гарантирано са уникални (от InterNIC,2 които контролират присвояването им) ако следвате то­ва договаряне имената на вашите package винаги ще бъдат уникални и няма да има конфликт на имена. (Докато някой друг вземе домейновото име от вас и за­почне да пише Java код със същите директорни пътища като вие сте писали.) Раз­бира се, ако нямате собствено домейново име трябва да измислите някаква уни­кална комбинация (като първото име и фамилията ви) за да се създадат уни­кал­ни имена на пакетите. Ако сте решили да публикувате Java код относително мал­кото усилие да вземете домейново име си струва.

Втората част от трика е разхвърлянето на package името в директория на ва­ша­та машина, та когато Java програма се пусне и трябва да се натовари .class файл (кое­то става динамично, в момента когато трябва да се създаде обект от въ­прос­ния клас или има достъп до static член на класа), тя да може да намери къ­де се намира .class файла.

Интерпретаторът на Java процедира по следния начин. Първо намира про­мен­ли­вата CLASSPATH от работната среда (която е сложена от операционната сис­те­ма когато Java или инструмент като Java-способен броузър се е инсталирал на ма­шината). CLASSPATH съдържа една или повече директории които служат за ко­рен при търсенето на .class файлове. Започвайки от него корен ин­тер­пре­та­то­рът ще вземе името и на мястото на всяка точка ще сложи наклонена черта за да формира пътя от CLASSPATH корена (така package foo.bar.baz става foo\bar\baz или foo/bar/baz в зависимост от операционната система). Това пос­ле се съединява с различните пътища в CLASSPATH. Каквото се получава — там се гледа за .class файл с име отговарящо на класа който се търси. (Пре­търс­ват се също и някои стандартни директории в зависимост от разположението на ин­терпретатора).

За да стане разбираемо, нека вземем моето домейново име което е bruceeckel.com. Като го обърнем, com.bruceeckel установява уникално гло­бал­но име за моите класове. (com, edu, org и т.н. разширението се капитализираше по-рано в Java пакетите, но това се промени в Java 2 така че цялото име на па­ке­та е с малки букви.) Мога да подразделя полученото по-нататък решавайки да съз­дам библиотека с име util, така че ще завърша с име на пакет:

package com.bruceeckel.util;

Сега това име на пакет може да бъде използвано като захлупващо за про­стран­ство­то на имената на следните два файла:

//: com:bruceeckel:util:Vector.java

// Creating a package

package com.bruceeckel.util;


public class Vector {

public Vector() {

System.out.println(

"com.bruceeckel.util.Vector");

}

} ///:~


Когато създавате свои пакети ще откриете, че package операторът трябва да бъ­де първият некоментарен ред във файла. Вторият сорс изглежда доста подо­бно:

//: com:bruceeckel:util:List.java

// Creating a package

package com.bruceeckel.util;


public class List {

public List() {

System.out.println(

"com.bruceeckel.util.List");

}

} ///:~


И двата файла се слагат в поддиректория на моята система:

C:\DOC\JavaT\com\bruceeckel\util

Ако погледнете отзад напред ще видите com.bruceeckel.util, но как е работата с пър­вата част на пътя? Тя е от CLASSPATH променливата която на моята ма­ши­на е:

CLASSPATH=.;D:\JAVA\LIB;C:\DOC\JavaT

Може да се види че CLASSPATH може да съдържа няколко алтернативни пътя. Оба­че при използване на JAR файлове има вариация. Трябва да се сложи името на JAR файла в classpath, не само пътят на който е разположен. Така за JAR на­ре­чен grape.jar classpath би включвал:

CLASSPATH=.;D:\JAVA\LIB;C:\flavors\grape.jar

Щом като веднъж пътят до класовете е установен вярно следния файл може да се сложи където и да е: (Виж стр.89 ако има проблеми с изпълнението на тази про­грама.):

//: c05:LibTest.java

// Uses the library

package c05;

import com.bruceeckel.util.*;

public class LibTest {

public static void main(String[] args) {

Vector v = new Vector();

List l = new List();

}

} ///:~



Когато компилаторът намери import оператора той започва търсене от ди­рек­то­риите споменати в CLASSPATH гледайки за поддиректория com\bruceeckel\util, а после преглежда файловете за подходящи имена (Vector.class за Vector и List.class за List). Забележете че и двата класа Vector и List трябва да са public.

Автоматична компилация


Първият път когато се създава обект от импортиран клас (или се прави достъп до static член на клас), компилаторът ще тръгне на лов за .class със същото име (та­ка че като създавате обект от клас X, търси се X.class) в съответната ди­рек­то­рия. Ако намери само X.class, това трябва да използва. Ако обаче намери X.java в същата директория, компилаторът ще сравни времената на създаване на двата файла и ако X.java е по-късно създаден от X.class ще прекомпилира автоматично X.java за да генерира усъвременен X.class.

Ако класът не е в .java файл със същото име като класа това поведение не е в си­ла за този клас.


Колизии


Какво става когато две библиотеки са импортирани със * и те включват ед­нак­ви имена? Например да предположим че програмата прави това:

import com.bruceeckel.util.*;

import java.util.*;

Тъй като java.util.* също съдържа клас Vector, имаме потенциална колизия. Оба­че докато колизията не възникне наистина всичко е OK – това е хубаво, за­що­то иначе ще трябва да се пишат много редове праграма за да се пред­от­вра­тят колизии които никога няма да възникнат.

Колизията се получава ако се опитате да направите Vector:

Vector v = new Vector();

За кой клас Vector става дума? Компилаторът не може да знае, а също и чи­та­те­лят. Така че компилаторът се сърди и ви кара да бъдете експлицитни. Ако искам стан­дартния Java Vector, например, трябва да напиша:

java.util.Vector v = new java.util.Vector();

Тъй като така (и с CLASSPATH) напълно се определя мястото на искания Vector, няма нужда от import java.util.* оператор освен ако не използвам нещо дру­го от java.util.

Библиотека собствени инструменти


С това знание можете да създавате собствени библиотеки с инструменти за да на­малите или елиминирате дублицирането на код. Например да създадем псев­до­ним на System.out.println( ) за да намалим писането. Това може да бъде част от пакет наречен tools:

//: com:bruceeckel:tools:P.java

// The P.rint & P.rintln shorthand

package com.bruceeckel.tools;


public class P {

public static void rint(Object obj) {

System.out.print(obj);

}

public static void rint(String s) {



System.out.print(s);

}

public static void rint(char[] s) {



System.out.print(s);

}

public static void rint(char c) {



System.out.print(c);

}

public static void rint(int i) {



System.out.print(i);

}

public static void rint(long l) {



System.out.print(l);

}

public static void rint(float f) {



System.out.print(f);

}

public static void rint(double d) {



System.out.print(d);

}

public static void rint(boolean b) {



System.out.print(b);

}

public static void rintln() {



System.out.println();

}

public static void rintln(Object obj) {



System.out.println(obj);

}

public static void rintln(String s) {



System.out.println(s);

}

public static void rintln(char[] s) {



System.out.println(s);

}

public static void rintln(char c) {



System.out.println(c);

}

public static void rintln(int i) {



System.out.println(i);

}

public static void rintln(long l) {



System.out.println(l);

}

public static void rintln(float f) {



System.out.println(f);

}

public static void rintln(double d) {



System.out.println(d);

}

public static void rintln(boolean b) {



System.out.println(b);

}

} ///:~



Всичките различни даннови типове могат сега да се извеждат с нов ред (P.rintln( )) или без нов ред (P.rint( )).

Може да познаете, че този фойл трябва да се намира на директория, която е на един от пътищата започващи в CLASSPATH , после продължава com/bruceeckel/tools. След компилирането P.class файла може да бъде из­полз­ван къде да е във вашата система чрез използването на import:

//: c05:ToolTest.java

// Uses the tools library

import com.bruceeckel.tools.*;
public class ToolTest {

public static void main(String[] args) {

P.rintln("Available from now on!");

}

} ///:~



Така от сега нататък щом се сдобиете с хубаво ютилити може да го сложите в tools директорията. (Или във вашата собствена util или tools директория.)

Капанът с classpath


Файлът P.java изважда наяве интересен капан. Особено в ранните реализации на Java слагането на коректен classpath изобщо си е главоболие. През време на раз­работката на тази книге файлът P.java беше създаден и тестван и си ра­бо­те­ше чудесно, но по едно време работата се развали. Дълго време си мислех че то­ва се дължи на грешка в реализацията на Java или някаква друга грешка, но на­края открих че в едната точка където бях въвел програмата (CodePackager.java, показан в глава 17) че се използва различен клас P. Понеже бе­ше използван като инструмент, той понякога биваше слаган в classpath. Ко­га­то фигурираше там, P в CodePackager.java беше намиран първо от Java когато се използваше програма и се гледаше в com.bruceeckel.tools и компилаторът каз­ваше, че конкретният метод липсва. Това беше разочароващо, понеже в гор­ния файл може да се види клас P и никаква допълителна информация не се из­веж­даше, която да подсети че се намира съвсем друг клас. (Който даже не беше public.)

На пръв поглед това изглежда като бъг на компилатора, но ако се погледне import той казва само “ето тука може да се намери P.” Обаче компилаторът ще гле­да навсякъде по classpath, така че P се използва от там, от където се намери, а в случая се намира “неправилен” клас първо по време на търсенето и то се пре­кратява. Това е малко по-различно от написаното Error: Reference source not found понеже и двата класа са си в пакетите и ето P което не беше в пакета, но можа да бъде намерено при търсенето по classpath.

Ако имате подобни преживявания, осигурете наличието само на един клас с ед­но име по classpath.

Използване на импортиране за промяна на поведението


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

Има обаче и други различни нужди от условна компилация. Често срещано е ком­пилирането на код за улесняване на тестването. Чертите за дебъгинга се включ­ват по време на разработката и се изключват за търговската версия. Allen Holub (www.holub.com) излезе с идеята да използва пакетите за подражаване на ус­лов­ната компилация. Той създаде Java версия на много полезния assertion механизъм на C, чрез който може да се каже “това ще е истина” или “това ще е лъ­жа” и ако операторът не е съгласен с вас ще си проличи. Такъв инструмент е мно­го полезен при тестването.

Ето класа който да се използва при тестване:

//: com:bruceeckel:tools:debug:Assert.java

// Assertion tool for debugging

package com.bruceeckel.tools.debug;


public class Assert {

private static void perr(String msg) {

System.err.println(msg);

}

public final static void is_true(boolean exp) {



if(!exp) perr("Assertion failed");

}

public final static void is_false(boolean exp){



if(exp) perr("Assertion failed");

}

public final static void



is_true(boolean exp, String msg) {

if(!exp) perr("Assertion failed: " + msg);

}

public final static void



is_false(boolean exp, String msg) {

if(exp) perr("Assertion failed: " + msg);

}

} ///:~


Този клас просто капсулира булеви тестове, които извеждат съобщение ако са отрицателни. В глава 9 ще научите за по-напреднал метод за работа при грешки на­­речен exception handling, а perr( ) методът и до там ще работи пер­фект­но.

Когато искате да използвате този клас добавяте реда:

import com.bruceeckel.tools.debug.*;

За да махнете твърденията и да може да разпространите кода, втори Assert се съз­дава, но в различен пакет:

//: com:bruceeckel:tools:Assert.java

// Turning off the assertion output

// so you can ship the program.

package com.bruceeckel.tools;


public class Assert {

public final static void is_true(boolean exp){}

public final static void is_false(boolean exp){}

public final static void

is_true(boolean exp, String msg) {}

public final static void

is_false(boolean exp, String msg) {}

} ///:~


Сега ако промените предишния import оператор на:

import com.bruceeckel.tools.*;

програмата няма повече да извежда твърденията. Ето пример:

//: c05:TestAssert.java

// Demonstrating the assertion tool

package c05;

// Comment the following, and uncomment the

// subsequent line to change assertion behavior:

import com.bruceeckel.tools.debug.*;

// import com.bruceeckel.tools.*;


public class TestAssert {

public static void main(String[] args) {

Assert.is_true((2 + 2) == 5);

Assert.is_false((1 + 1) == 2);

Assert.is_true((2 + 2) == 5, "2 + 2 == 5");

Assert.is_false((1 + 1) == 2, "1 +1 != 2");

}

} ///:~


Чрез промяна на импортирания пакет package може да смените кода от дебъг вер­сията към продукционната версия. Тази техника може да се използва за все­ки вид условен код.

Относно пакетите


Заслужава си да се помни, че щом се създаде пакет неявно се задава път. Па­ке­тът трябва да е наличен в директорията зададена от името му, като тя трябва да може да се намери като се започне от CLASSPATH. Експериментирането с клю­човата дума package може да бъде малко разочароващо отначало, понеже до­като не спазите условието за зависимостта между името на файла и пътя ще по­лучавате много мистериозни съобщения че даден клас не може да се намери, да­же и класът да си е там в директорията. Ако получите подобно съобщение опи­тайте да изкоментирате package и ако тръгне ще знаете къде е проблемът.




Сподели с приятели:
1   ...   24   25   26   27   28   29   30   31   ...   73




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

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