Резюме
Полиморфизъм значи “различни форми.” В ООП имате същото лице (общия интерфейс на базовия клас) и различни форми използващи това лице: различните версии на динамично свързваните методи.
Видяхте в тази глава че не е възможно да се разбере и даже да се създаде пример за полиморфизъм без използването на абстракция на данните и наследяване. Полиморфизмът е черта която не може да се разглежда изолирано (като switch оператора например), а работи само в концерт, като част от “голямата картина” на зависимостите между класовете. Хората често се смущават от други, не ОО черти на Java, като претоварването на методи, които понякога биват представяни като обектно ориентирани. Не се лъжете: ако няма късно свързване няма полиморфизъм.
За да се използва полиморфизъм и с това ОО черти ефективно във вашите програми трябва да си разширите кръгозора не извън само методите и съобщенията на отделен клас, но също и общо между класовете и техните взаимозависимости. Въпреки че това изисква значителни усилия, тази борба си заслужава, понеже дава по-бърза разработка на програмите, по-добра организация на кода, разширяеми програми, по-лесна поддръжка на кода.
Упражнения -
Създайте йерархия на наследяване от Rodent: Mouse, Gerbil, Hamster и т.н.. В базовия клас дайте методи които са общи за Rodent-ите, подтиснете ги в извлечените класове за да осигурите специфичното за дадения Rodent поведение. Създайте масив Rodentи, попълнете го със специфични типове Rodentи, извикайте методите на базовия клас да видите какво ще стане.
-
Променете упражнение 1 така че Rodent да е interface.
-
Оправете проблема в WindError.java.
-
В GreenhouseControls.java добавете Event вътрешни класове които включват и изключват вентилаторите.
8: Притежаване на обектите
Твърде проста е програма, която има фиксиран брой обекти с известни времена на живот.
Изобщо програмите ви ще създават обекти според критерии, които ще са известни чак по време на изпълнение на програмите. Няма да знаете докато програмата се пусне броя и даже точния тип на нужните обекти. За да се реши общия програмен проблем трябва да може да създавате произволно число обекти, по всяко време, навсякъде. Така че не може да се разчита на създаване на именуван манипулатор за всеки от обектите:
MyObject myHandle;
понеже никога не се знае точно колко броя ще трябват.
За да се реши този доста основен проблем, Java има няколко основни начина за владеене на обекти (или по-скоро - на манипулатори). Вграденият тип е масив, който беше коментиран преди и ще се спрем още на него в тази глава. Също помощната библиотека на Java има класове-колекции (също известни като контейнерни класове, но терминът “контейнер” е използван от Swing GUI библиотеката така че тук ще се използва “колекция”) които дават по-съвършен начин за владеене и даже за манипулиране на обекти. Останалата част от тази глава ще се занимава с тези неща.
Масиви
Повечето необходимо въведение в масивите се съдържа в последната част на глава 4, която показва как се дефинира и инициализира масив. Фокусът на тази глава е владеенето на обекти, а масивът е просто един начин да се направи това. Но има множество начини да се направи това, така че какво откроява масивите?
Има две неща, които отличават масивите сред другите колекции: ефективността и типа. Масивът е най-ефикасния начин който Java дава за запомняне и достъп до обекти (фактически - до манипулатори). Масивът е проста линейна последователност, което прави достъпа до елементи бърз, но се плаща за тази скорост: когато създавате масив, дължината му е зададена и не може да се променя по време на живота му (той е обект - б.пр.). Може да искате да създадете масив с определена дължина и после, ако не ви стигне мястото, да създадете нов и да преместите ванипулаторите от стария масив в новия. Това е поведението на класа ArrayList който ще бъде изучен по-късно в тази глава. Поради допълнителната работа заради тази гъвкавост с дължината, обаче, ArrayList е забележимо по-малко фективен от масивите.
Класът vector в C++ знае типа на обектите които съдържа, но това е допълнителен недостатък в сравнение с масивите в Java: Операторът на vector в C++ operator[] не прави проверка за излизане от обхвата, така че може да минете зад края. (Възможно е, обаче, да питате колко е голям vector и методът at( ) прави проверка да не се излиза извън границите.) В Java има проверка на границите независимо дали работите с масив или колекция – ще се получи RuntimeException ако излезете от границите. Както ще научите в глава 9, този тип изключение индицира програмистка грешка и затова не е необходимо да проверявате за него в програмата. Впрочем, причината в C++ vector да не проверява за прескачане на границите при всеки достъп е скоростта – в Java имаме постоянни допълнителни разходи на ресурси както за масивите, така и за колекциите.
Другите родови класове за колекции, които ще се изучават в тази глава, List, Set и Map, работят с обектите като че последните нямат специфичен тип. Тоест те ги третират като да са Object, кореновият клас на класовете в Java. Това работи чудесно от една гледна точка: необходимо е да построите само една колекция, който и да е Java обект ще е подходящ за нея. (Освен примитивите – те могат да бъдат слагани като константи в колекциите чрез обгръщащите класове в Java или като променяеми величини като ги включите в свой клас.) Това е второто място където масивът е най-добър за родови колекции: когато създавате масив го създавате да съдържа определен тип. Това значи че имате проверка по време на компилация да не сложите в него неправилен тип, или да не сбъркато типа който извлимчате. Разбира се, Java няма да ви позволи да изпротите неподходящо съобщение към обект, както (ще провери типа-б.пр.) по време на компилация, така и по време на изпълнение. Така че едното не е по-рисковано от другото; просто едното е по-бързо, по-добре е компилаторът да се обади, тогава и вероятността крайният потребител да получи изключение е по-малка.
Заради ефективността и проверката на типовете е най-добре да използвате масиви ако е възможно. Ако обаче имате да решавате по-общ проблем масивите са твърде ограничаващи. След погледа към масивите останалата част от тази глава ще бъде посветена на колекциите, доставяни с Java.
Масивите са първокласни обекти
Независимо от типа на масива с който работите идентификаторът на масива е фактически манипулатор към обект създаден на хийпа. Обектът в хийпа може да бъде създаден или имплицитно, като част от синтаксиса на инициализацията, или явно с new израз. Част от тобект на хийпа (фактически единственото поле или метод който е достъпен) е членът само за четене length който казва колко елемента могат да бъдат запомнени. Синтаксисът ‘[]’ е единственият друг достъп който маже да има до елементите на масив.
Следващият пример показва различни начини за инициализиране на масиви и как манипулаторите на масиви могат да бъдат присвоявани на други масиви-обекти. Също се показва че масивите от примитиви и масивите от обекти са почти еднакви откъм използване. Единствената разлка е че часивите от обекти съдържат манипулатори докато тези от примитиви съдържат направо стойности. (Виж стр. 89 ако има проблеми с изпълнението на тази програма.)
//: c08:ArraySize.java
// Initialization & re-assignment of arrays
package c08;
class Weeble {} // A small mythical creature
public class ArraySize {
public static void main(String[] args) {
// Arrays of objects:
Weeble[] a; // Null handle
Weeble[] b = new Weeble[5]; // Null handles
Weeble[] c = new Weeble[4];
for(int i = 0; i < c.length; i++)
c[i] = new Weeble();
Weeble[] d = {
new Weeble(), new Weeble(), new Weeble()
};
// Compile error: variable a not initialized:
//!System.out.println("a.length=" + a.length);
System.out.println("b.length = " + b.length);
// The handles inside the array are
// automatically initialized to null:
for(int i = 0; i < b.length; i++)
System.out.println("b[" + i + "]=" + b[i]);
System.out.println("c.length = " + c.length);
System.out.println("d.length = " + d.length);
a = d;
System.out.println("a.length = " + a.length);
// Java 1.1 initialization syntax:
a = new Weeble[] {
new Weeble(), new Weeble()
};
System.out.println("a.length = " + a.length);
// Arrays of primitives:
int[] e; // Null handle
int[] f = new int[5];
int[] g = new int[4];
for(int i = 0; i < g.length; i++)
g[i] = i*i;
int[] h = { 11, 47, 93 };
// Compile error: variable e not initialized:
//!System.out.println("e.length=" + e.length);
System.out.println("f.length = " + f.length);
// The primitives inside the array are
// automatically initialized to zero:
for(int i = 0; i < f.length; i++)
System.out.println("f[" + i + "]=" + f[i]);
System.out.println("g.length = " + g.length);
System.out.println("h.length = " + h.length);
e = h;
System.out.println("e.length = " + e.length);
// Java 1.1 initialization syntax:
e = new int[] { 1, 2 };
System.out.println("e.length = " + e.length);
}
} ///:~
Ето изхода от тази програма:
b.length = 5
b[0]=null
b[1]=null
b[2]=null
b[3]=null
b[4]=null
c.length = 4
d.length = 3
a.length = 3
a.length = 2
f.length = 5
f[0]=0
f[1]=0
f[2]=0
f[3]=0
f[4]=0
g.length = 4
h.length = 3
e.length = 3
e.length = 2
Масивът а a е просто манипулатор сочещ null и компилаторът предотвратява опити да се прави с него нещо преди да е провилно инициалициран. Масивът b се инициалицира да сочи масив от Weeble манипулатори, но никога не се слагат там Weeble обекти. Може обаче да питате колко е дължината на масива, тъй като b сочи законен обект. Това извежда наяве малък недостатък: не може да намерите колко елемента има в масива, понеже length казва само колко елемента може да бъдат сложени в масива; тоест, дължината на обекта-масив, не колко елемента съдържа. Обаче когато се създава обект неговия манипулатор се инициализира с null така че можете да видите дали даден конкретен масив има елементи като проверите дали манипулаторът му сочи null. По подобен начин всеки масив от примитиви се инициализира с нула за числените типове, null за char и false за boolean.
Масивът c показва създаване на масив от обекти и присвояването на Weeble обекти на всичките слотове на масива. Масивът d показва синтаксиса на “агрегираната инициализация” с който се получава създадане на обект-масив (неявно с new на хийпа, точно като масива c) и инициализиране с обекти Weeble, всичкото в един ред.
Изразът
a = d;
как може да се вземе манипулатор присъединен към един масив-обект и да ме се присвои друг обект-масив, точно както може да се направи с всеки един манипулатор на обект. Сега както a така и d сочат един и същ масив върху хийпа.
Java 1.1 добавя нов синтаксис за инициализация на масиви, която може да си представим като “динамична агрегатна инициализация.” Агрегатната инициализация от Java 1.0 използвана за d трябва да бъде използвана в точката на дефиниция на d, но със синтаксиса на Java 1.1 може да създавате и инициализирате масиви навсякъде. Например да предположим че hide( ) е метод който приема масив от Weeble обекти. Бихте могли да напишете:
hide(d);
но в Java 1.1 може също динамично да създадете масива който ще дадете като аргумент:
hide(new Weeble[] { new Weeble(), new Weeble() });
Този нов синтаксис дава по-удобен начин за писане на кода в някои ситуации.
Втората част от примера показва че масив от примитиви работи точно както масив от обекти освен че масивът от примитиви съдържа директно стойности.
Колекции от примитиви
Колекцийните класове могат да съдържат само манипулатори към обекти. Масив, обаче, може да бъде създаден да съдържа директно стойности, точно както може да съдържа и манипулатори към масиви. Възможно е да се използват “обхващащи” класове като Integer, Double и т.н. за да се сложат стойности на примитиви вътре в колекция, но както ще видите по-късно в тази глава в примера WordCount.java обгръщащите класове са малко полезни в масиви. Дали да сложите примитиви в масив или да ги обгърнете в класове и да ги сложите в колекции е въпрос на ефективност. Много по-ефективно е да сложите стойностите в масив отколкото класове в колекция.
Разбира се, ако вашата колекция трябва сама да може да се разширява, макар и да е от примитиви, трябва да използвате обгръщащите класове. Може да си мислите че трябва да има специализиран тип Vector за всеки от примитивните типове данни, но Java не прави това за нас. Някакъв вид шаблони могат да дадат един ден по-добър начина за решаване на този проблем в Java.1
Връщане на масив
Да предположим, че пишете метод и искате да върнете не едно нещо, а цял куп неща. Езици като C и C++ не улесняват това, защото не може да се върне направо масив, само указател към масив. Това докарва проблеми защото е трудно да се следи времето на живот на масив, което лесно води до изтичания на памет.
Java приема подобен подход, но просто се “връща масив.” В действителност, разбира се, се връща манипулатор към масив, но с Java нямате отговорност за този масив – той ще бъде налице колкото ви трябва, а боклучарят ще почисти когато му дойде времето.
Като пример да вземем връщането на масив от тип String:
//: c08:IceCream.java
// Returning arrays from methods
public class IceCream {
static String[] flav = {
"Chocolate", "Strawberry",
"Vanilla Fudge Swirl", "Mint Chip",
"Mocha Almond Fudge", "Rum Raisin",
"Praline Cream", "Mud Pie"
};
static String[] flavorSet(int n) {
// Force it to be positive & within bounds:
n = Math.abs(n) % (flav.length + 1);
String[] results = new String[n];
boolean[] picked =
new boolean[flav.length];
for (int i = 0; i < n; i++) {
int t;
do
t = (int)(Math.random() * flav.length);
while (picked[t]);
results[i] = flav[t];
picked[t] = true;
}
return results;
}
public static void main(String[] args) {
for(int i = 0; i < 20; i++) {
System.out.println(
"flavorSet(" + i + ") = ");
String[] fl = flavorSet(flav.length);
for(int j = 0; j < fl.length; j++)
System.out.println("\t" + fl[j]);
}
}
} ///:~
Методът flavorSet( ) създава масив от String наречен results. Дължината на този масив е n, определена от аргумента който се дава на метода. После продължава случайното избиране измежду flav и слагането им в results, което накрая се връща. Връщането на масив е точно като връщането на всеки един обект – връща се манипулатор. Не е важно че масивът бе създаден в flavorSet( ), нито ако е бил създаден където и да е другаде, в този смисъл. Боклучарят се грижи за махането на масива когато вече не е нужен, а той ще стои докато е нужен.
Между другото, забележете че flavorSet( ) избира по случаен начин като осигурява да няма избиране два пъти на един елемент. Това се прави в do цикъл който продължава избора докато намери че последното избрано не присъства в масива picked. (Разбира се, сравняването на String също би могло да бъде използвано, но сравняването на String е неефективно.) Ако изборът е успешен, добавя се. (i се инкрементира).
main( ) извежда 20 пълни множества вкус, така че може да видите че flavorSet( ) използва случайно избиране всеки път. Това е по-лесно да се наблюдава ако се пренасочи изходът към файл. И, докато гледате файла, помнете, не сте наистина гладни. (Вие само искате сладоледа, вие не се нуждаете от него в действителност.)
Сподели с приятели: |