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



страница24/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   20   21   22   23   24   25   26   27   ...   73

Резюме


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

Упражнения


  1. Напишете програма която извежда стойности от едно до 100.

  2. Променете упражнение 1 така че програмата да завършва чрез break оператор при стойност 47. Опитайте с return след това.

  3. Създайте switch оператор който извежда съобщение с всеки case и сложете switch вътре във for цикъл който опитва всеки case. Сложете break след всеки case и го изпробвайте, после махнете breakовете и вижте какво ще стане.


4: Инициализация и почистване


С напредването на компютърната революция “небезо­пас­ното” програмиране е кганало главния обви­ня­ем по скъпотията на програмирането.

Два от въпросите на безопасността са инициализацията и почистването. Мно­го C бъгове стават поради забравянето от програмиста да инициализира про­мен­лива. Така е особено с библиотеките, където програмистът не знае как да инициа­лизира променлива или пък че въобще трябва да го прави. Почистването пък е още по-голям проблем, защото човек рядко се сеща за някакъв елемент, кой­то вече не му трябва. Така ресурсите използване от въпросния елемент оста­ват заети и лесно се стига до липса на ресурси (обикновено памет).

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

Гарантирана инициализация с конструктора


Може да си въобразим създаване на метод наречен initialize( ) за всеки клас кой­то създаваме. Името подсказва че методът се вика преди да се създаде обек­та. За нещастие потребителят трябва да помни, че трябва да извика метода. В Java проектатнтът на класа може да гарантира инициализацията със специален ме­тод, наречен constructor. Ако класа има конструктор, Java автоматично го ви­ка при създаването на обект преди потребителите да могат да го пипнат с пръст. Така инициализацията е гарантирана.

Следващото предизвикателство е как да наречем този метод. Има две неща. Първото е че каквото и име да изберете то може да е в противоречие с името, кое­то бихте използвали за някой член. Другото е, че понеже компилаторът е отго­ворен за викането на конструктора, трябва винаги да му е известно кой имен­но метод да извика. Решението в C++ изглежда най-лесно и логично и се из­ползва също и в Java: Името на конструктора е същото като името на класа. Смис­лено е такъв член да се вика именно при инициализацията.

Ето простичък клас с конструктор: (Вижте стр. 89 ако имате проблем с пус­ка­не­то на тази програма.)

//: c04:SimpleConstructor.java

// Demonstration of a simple constructor

package c04;


class Rock {

Rock() { // This is the constructor

System.out.println("Creating Rock");

}

}


public class SimpleConstructor {

public static void main(String[] args) {

for(int i = 0; i < 10; i++)

new Rock();

}

} ///:~


Сега, когато обектът е създаден:

new Rock();

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

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

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

class Rock {

Rock(int i) {

System.out.println(

"Creating Rock number " + i);

}

}


public class SimpleConstructor {

public static void main(String[] args) {

for(int i = 0; i < 10; i++)

new Rock(i);

}

}

Аргументите на конструктора дават възможност да се управлява инициализацията. Например ако класът Tree има единствен числен аргумент показващ височината на дървото, обектът от тип Tree ще се създава така:



Tree t = new Tree(12); // 12-foot tree

Ако Tree(int) е единственият конструктор, компилаторът няма да позволява съз­даването на Tree обект по никакъв друг начин.

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

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


Пренатоварване на методи


Една важна черта на всеки програмен език е използването на имената. Когато се съз­дава обект се дава име на област от паметта. Методът е име на действие. Чрез използване на имената за описание на системата вие създавате програма коя­то е лесна за разбиране от хората и за променяне. Много прилича на писане на проза ­­– целта е комуникацията с читателите.

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

Проблемът изниква когато се проектира концепцията за нуанса на човешкия език върху програмния език. Често една и съща дума се използва в различни зна­чения – тя се пренатоварва (английската дума е "претоварва", но до сега из­бяг­вах точния превод за да не изглежда, че нещо се претоварва и ще се счупи, при­мерно. По-нататък ще използвам и двата превода - бел.пр.). Това е полезно осо­бено когато се отнася за тривиални разлики. Казваме “измий лицето,” “измий колата,” “измий кучето.” Би било тъпо да се налага да се казва “faceWash лицето,” “carWash колата,” и “dogWash кучето” само за да може слу­ша­телят да направи разликата. Повечето човешки езици са с излишък и ако се из­пуснат няколко думи смисълът остава разбираем. Не се нуждаем от уникални иден­тификатори – значението може да се изведе от контекста.

Повечето програмни езици (C в частност) изискват да има уникален иден­ти­фи­ка­тор за всяка функция. Не може да има една функция наречена print( ) за пе­ча­та­не на цели числа и друга наречена print( ) за такива с плаваща запетая – всяка функ­ция изисква уникално име.

Друг фактор налага претоварването на имената в Java: конструкторът. Понеже име­то на конструктора е същото като на класа може да има само едно име на кон­структор. Ами ако искаме да създаваме обекта по повече начини? Да кажем ис­каме да създадем обект който се инициализира по стандартен начин или взема данните от файл. Трябват два конструктора, единият няма аргументи (кон­структорът по подразбиране) и един с аргумент String който е името на файла от който ще се вземат данните за инициализацията. И двата са кон­струк­то­ри, така че трябнва да имат едно и също име – името на класа. Така пре­то­вар­ването на методите е необходимо за да позволи едно име да се използва с раз­лични типове аргументи. И освен че претоварването на методите е неиз­беж­но при конструкторите, то е много удобно и се използва за всякакви методи.

Ето пример показващ претоварването на конструктори и обикновени методи:

//: c04:Overloading.java

// Demonstration of both constructor

// and ordinary method overloading.

import java.util.*;


class Tree {

int height;

Tree() {

prt("Planting a seedling");

height = 0;

}

Tree(int i) {



prt("Creating new Tree that is "

+ i + " feet tall");

height = i;

}

void info() {



prt("Tree is " + height

+ " feet tall");

}

void info(String s) {



prt(s + ": Tree is "

+ height + " feet tall");

}

static void prt(String s) {



System.out.println(s);

}

}


public class Overloading {

public static void main(String[] args) {

for(int i = 0; i < 5; i++) {

Tree t = new Tree(i);

t.info();

t.info("overloaded method");

}

// Overloaded constructor:



new Tree();

}

} ///:~



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

Може също да искате да викате info( ) методът по повече от един начин. На­при­мер със String аргумент може да искате извеждане на допълнително съобщение и без нищо ако нямате какво да кажете. Би било странно ако се налага да се да­ват различни имена на неща от една концепция. За щастие претоварването на ме­тодите позволява едно име и за двата случая.


Различаване на претоварените методи


Ако методите имат едно име, как да определи Java кой метод имате пред вид? Пра­вилото е просто: Всеки претоварен метод трябва да има уникален списък ар­гу­менти.

Ако помислите за секунда ще видите, че следното има смисъл: как по друг на­чин, освен този с аргументите, програмистът би могъл да зададе разликата меж­ду методите?

Даже промяната на реда на аргументите може да послужи за определяща раз­ли­ка: (Макар че този подход обикновено се избягва, защото води до труден за под­държане код.)

//: c04:OverloadingOrder.java

// Overloading based on the order of

// the arguments.


public class OverloadingOrder {

static void print(String s, int i) {

System.out.println(

"String: " + s +

", int: " + i);

}

static void print(int i, String s) {



System.out.println(

"int: " + i +

", String: " + s);

}

public static void main(String[] args) {



print("String first", 11);

print(99, "Int first");

}

} ///:~


Двата print( ) имат идентични аргументи, но редът им е различен и това е, кое­то прави методите различими.

Претоварването и примитивите


Примитивните типове могат да бъдат разширявани при операции с тях и това е сму­щаващо във връзка с претоварването. Следващият пример показва какво ста­ва, когато примитив се даде на претоварен метод:

//: c04:PrimitiveOverloading.java

// Promotion of primitives and overloading
public class PrimitiveOverloading {

// boolean can't be automatically converted

static void prt(String s) {

System.out.println(s);

}
void f1(char x) { prt("f1(char)"); }

void f1(byte x) { prt("f1(byte)"); }

void f1(short x) { prt("f1(short)"); }

void f1(int x) { prt("f1(int)"); }

void f1(long x) { prt("f1(long)"); }

void f1(float x) { prt("f1(float)"); }

void f1(double x) { prt("f1(double)"); }
void f2(byte x) { prt("f2(byte)"); }

void f2(short x) { prt("f2(short)"); }

void f2(int x) { prt("f2(int)"); }

void f2(long x) { prt("f2(long)"); }

void f2(float x) { prt("f2(float)"); }

void f2(double x) { prt("f2(double)"); }


void f3(short x) { prt("f3(short)"); }

void f3(int x) { prt("f3(int)"); }

void f3(long x) { prt("f3(long)"); }

void f3(float x) { prt("f3(float)"); }

void f3(double x) { prt("f3(double)"); }
void f4(int x) { prt("f4(int)"); }

void f4(long x) { prt("f4(long)"); }

void f4(float x) { prt("f4(float)"); }

void f4(double x) { prt("f4(double)"); }


void f5(long x) { prt("f5(long)"); }

void f5(float x) { prt("f5(float)"); }

void f5(double x) { prt("f5(double)"); }
void f6(float x) { prt("f6(float)"); }

void f6(double x) { prt("f6(double)"); }


void f7(double x) { prt("f7(double)"); }
void testConstVal() {

prt("Testing with 5");

f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);

}

void testChar() {



char x = 'x';

prt("char argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testByte() {



byte x = 0;

prt("byte argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testShort() {



short x = 0;

prt("short argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testInt() {



int x = 0;

prt("int argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testLong() {



long x = 0;

prt("long argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testFloat() {



float x = 0;

prt("float argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

void testDouble() {



double x = 0;

prt("double argument:");

f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);

}

public static void main(String[] args) {



PrimitiveOverloading p =

new PrimitiveOverloading();

p.testConstVal();

p.testChar();

p.testByte();

p.testShort();

p.testInt();

p.testLong();

p.testFloat();

p.testDouble();

}

} ///:~


Ако разгледате изхода от тази програма ще видите, че константата 5 се третира ка­то int, така че ако преторавен метод има за аргумент int той се използва. Във всич­ки други случаи, ако имате даннов тип по-малък от този в метода, той се раз­ширява. char дава малко по-различен ефект, понеже ако не се намери точно char съвпадение той се разширява до int.

Какво става, ако вашият аргументи е по-голям от очаквания от претоварен ме­тод? Отговорът се дава от модификация на предишната програма:

//: c04:Demotion.java

// Demotion of primitives and overloading


public class Demotion {

static void prt(String s) {

System.out.println(s);

}
void f1(char x) { prt("f1(char)"); }

void f1(byte x) { prt("f1(byte)"); }

void f1(short x) { prt("f1(short)"); }

void f1(int x) { prt("f1(int)"); }

void f1(long x) { prt("f1(long)"); }

void f1(float x) { prt("f1(float)"); }

void f1(double x) { prt("f1(double)"); }


void f2(char x) { prt("f2(char)"); }

void f2(byte x) { prt("f2(byte)"); }

void f2(short x) { prt("f2(short)"); }

void f2(int x) { prt("f2(int)"); }

void f2(long x) { prt("f2(long)"); }

void f2(float x) { prt("f2(float)"); }


void f3(char x) { prt("f3(char)"); }

void f3(byte x) { prt("f3(byte)"); }

void f3(short x) { prt("f3(short)"); }

void f3(int x) { prt("f3(int)"); }

void f3(long x) { prt("f3(long)"); }
void f4(char x) { prt("f4(char)"); }

void f4(byte x) { prt("f4(byte)"); }

void f4(short x) { prt("f4(short)"); }

void f4(int x) { prt("f4(int)"); }


void f5(char x) { prt("f5(char)"); }

void f5(byte x) { prt("f5(byte)"); }

void f5(short x) { prt("f5(short)"); }
void f6(char x) { prt("f6(char)"); }

void f6(byte x) { prt("f6(byte)"); }


void f7(char x) { prt("f7(char)"); }
void testDouble() {

double x = 0;

prt("double argument:");

f1(x);f2((float)x);f3((long)x);f4((int)x);

f5((short)x);f6((byte)x);f7((char)x);

}

public static void main(String[] args) {



Demotion p = new Demotion();

p.testDouble();

}

} ///:~


Тук методите получават по-малки типове. Ако вашият аргумент е по-голям тип тряб­ва да го cast-нете към необходимия тип използвайки името в скоби. Ако не на­правите това, компилаторът ще издаде съобщение за грешка.

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


Претоварване на връщаните стойности


Често се чудят “Защо само имената на класовете и списъците на аргументите? Защо да не различаваме методите по връщаните стойностти?” Например два ме­тода които имат еднакви имена и аргументи могат да бъдат ясно отличени:

void f() {}

int f() {}

Това работи добре докато компилаторът може да определи каквото му трябва от контекста, като int x = f( ). Може обаче да се извика метод и да се игнорира връ­щаната стойност; това често се нарича извикване на метод заради стра­нич­ния му ефект тъй като въобще не ви трябва връщаната стойност а се инте­ре­­сувате само от страничния ефект. Например викайки метода по следния на­чин:

f();

Как би могъл компилаторът да определи кое f( ) ще се вика? Как би могъл да опре­дели това и кой да е читател? Поради този сорт проблеми не може да се раз­личават в езика метди по връщаната стойност.


Конструктори по подразбиране


Както беше споменато преди, конструкторът по подразбиране е този без ар­гу­мен­ти. Ако създадете клас без конструктори компилаторът ще създаде ав­то­ма­тич­но конструктор по подразбиране заради вас. Например:

//: c04:DefaultConstructor.java


class Bird {

int i;


}
public class DefaultConstructor {

public static void main(String[] args) {

Bird nc = new Bird(); // default!

}

} ///:~



Редът

new Bird();

създава нов обект и вика конструктор по подразбиране, въпреки че такъв не е яв­но деклариран. Без него нямаше да имаме начин да построим нашия обект. Ако обаче дефинирате някакви конструктори (с или без аргументи), ком­пи­ла­то­рът няма да синтезира конструктор по подразбиране вместо вас:

class Bush {

Bush(int i) {}

Bush(double d) {}

}

Ако сега напишем:



new Bush();

компилаторът ще се оплаква, че не може да намери подходящ конструктор. Все едно че ако не запишете никакви конструстори компилаторът казва “Трябва да из­ползвате някакъв конструктор, така че нека да направя един вместо вас.” Но ако напишете конструктор компилаторът казва “Написали сте конструктор зна­чи знаете какво правите; ако не сте сложили конструктор по подразбиране това ще е защото не искате да има.”


Ключовата дума this


Ако имате два обекта от един и същ тип наречени a и b може да се чудите как ли ще се вика f( ) за тези два обекта:

class Banana { void f(int i) { /* ... */ } }

Banana a = new Banana(), b = new Banana();

a.f(1);


b.f(2);

Ако има само един метод наречен f( ), как той знае дали е викан от a или b?

За да се позволи да се пише код по удобен ОО синтаксис в който се “изпраща съ­общение на обект,” компилаторът върши скрито работа заради вас. Има тай­на в първия аргумент на f( ) и тя е, че този аргумент е манипулатор на обекта, с кой­то работите. Така че двете викания на метода стават нещо като:

Banana.f(a,1);

Banana.f(b,2);

Това става вътрешно и не може да се напишат изразите и компилаторът да ги въз­приеме, но се дава идея за нещата.

Да предположим че сме вътре в обект и искаме да вземем манипулатора му. Тъй като той се дава скрито от компилатора, няма идентификатор за него. Има оба­че ключова дума за тази цел: this. Ключовата дума this – която може да се из­ползва само вътре в метод – дава манипулатора на обекта, от който методът е из­викан. Това е манипулатор и може да се третира като всеки друг. Помнете, че ако викате метод от ваш клас отвътре на друг метод на ваш клас не е не­об­хо­ди­мо да използвате this; просто викате метода. Текущия this манипулатор ав­то­ма­тично се използва за другия метод. Така може да се напише:

class Apricot {

void pick() { /* ... */ }

void pit() { pick(); /* ... */ }

}

В pit( ) може да се напише this.pick( ) но не е необходимо. Компилаторът го пра­ви автоматично. Ключовата дума this се използва само в онези специални слу­чаи, когато трябва явно да се използва манипулаторът на текущия обект. На­пример често се използва в return операторите когато се иска да се върне ма­нипулатор към текущия обект:



//: c04:Leaf.java

// Simple use of the "this" keyword


public class Leaf {

private int i = 0;

Leaf increment() {

i++;


return this;

}

void print() {



System.out.println("i = " + i);

}

public static void main(String[] args) {



Leaf x = new Leaf();

x.increment().increment().increment().print();

}

} ///:~


Понеже increment( ) връща манипулатор към текущия обект чрез ключовата ду­ма this многократно може да се изпълнят оператори върху същия обект.

Викане на конструктори от конструктори


Когато се пишат няколко конструктора за един клас понякога е удобно да се ви­ка конструктор от конструктор, за да се избегне дублирането на код. Това мо­же да се направи с ключовата дума this.

Нормално кагато се напише this то е в смисъл на “този обект” или “текущия обект” и произвежда манипулатор към текущия обект. В конструктор this има друг смисъл ако дадете списък от аргументи: тя прави явно викане на кон­струк­тор с този списък аргументи. Така имате праволинеен начин да викоте кон­струк­тор от конструктор:

//: c04:Flower.java

// Calling constructors with "this"


public class Flower {

private int petalCount = 0;

private String s = new String("null");

Flower(int petals) {

petalCount = petals;

System.out.println(

"Constructor w/ int arg only, petalCount= "

+ petalCount);

}

Flower(String ss) {



System.out.println(

"Constructor w/ String arg only, s=" + ss);

s = ss;

}

Flower(String s, int petals) {



this(petals);

//! this(s); // Can't call two!

this.s = s; // Another use of "this"

System.out.println("String & int args");

}

Flower() {



this("hi", 47);

System.out.println(

"default constructor (no args)");

}

void print() {



//! this(11); // Not inside non-constructor!

System.out.println(

"petalCount = " + petalCount + " s = "+ s);

}

public static void main(String[] args) {



Flower x = new Flower();

x.print();

}

} ///:~


Конструкторът Flower(String s, int petals) показва, че докато можете да викате един конструктор чрез this, не може да викате два. В добавка викането на кон­струк­тора трябва да е първото нещо, което се прави, иначе се получава съоб­ще­ние за грешка.

Този пример също показва друг начин за използване на this. Тъй като името на ар­гумента s и името на члена-данни s е същото, има двусмислие. То може да се раз­реши като се напише this.s за члена данни. Често ще видите тази форма в Java код и тя е използвана множество пъти в тази книга.

В print( ) може да видите, че компилаторът не ще ви позволи да викате кон­струк­тор от друг метод освен от конструктор.

Значението на static


С ключовата дума this на ум може по-пълно да се разбере значението на static. То е че няма this за конкрутния метод. Не може да се викат не-static извътре на static методи2 (макар че обратното е възможно) и може да се вика static за са­мия клас, без никакъв обект. Фактически най-вече за това са и static методите. То­ва е като да се прави еквивалент на глобална функция (в C). Глобалните функ­ции не са позволени в Java и писането на static вътре в клас позволява да са достъпни други static методи и static полета.

Някои хора спорят че static методите не са ОО понеже имат семантиката на гло­балните функции; със static метод не изпращате съобщение на обект, по­не­же няма this. Това е аргумент и ако установите че използвате много статични ме­тоди вероятно ще е добре да си преосмислите стратегията. Обаче static не­ща­та са прагматични и понякога истински се нуждаете от тях, така че и да са и да не са “правилно ОО” въпроса ще оставим на теоретиците. Разбира се, даже Smalltalk има еквиваленти с неговите “class methods.”





Сподели с приятели:
1   ...   20   21   22   23   24   25   26   27   ...   73




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

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