Резюме
Тази глава завършва изучаването на основните черти на повечето програмни езици: изчисления, приоритет на операторите, превръщане на типовете и селекция и итерация. Сега сте готови за стъпки които ще ви приближат към ОО програмиране. Следващата глава ще разгледа важния въпрос за инициализацията и почистването на обекти, а по-следващата фундаменталната концепция за скриване на код.
Упражнения -
Напишете програма която извежда стойности от едно до 100.
-
Променете упражнение 1 така че програмата да завършва чрез break оператор при стойност 47. Опитайте с return след това.
-
Създайте switch оператор който извежда съобщение с всеки case и сложете switch вътре във for цикъл който опитва всеки case. Сложете break след всеки case и го изпробвайте, после махнете breakовете и вижте какво ще стане.
С напредването на компютърната революция “небезопасното” програмиране е кганало главния обвиняем по скъпотията на програмирането.
Два от въпросите на безопасността са инициализацията и почистването. Много 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.”
Сподели с приятели: |