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



страница47/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   43   44   45   46   47   48   49   50   ...   73

Типове колекции


Стандартните библиотеки на Java 1.0 и 1.1 идват с възможния минимум кла­со­ве-колекции, но те вероятно са достатъчни за болшинството ваши програмни проек­ти. (Както ще видите на края на тази глава Java 2 има радикално пре­ра­бо­те­на и попълнена библиотека.)

ArrayList


ArrayList е доста прост за използване, както вече видяхме. Въпреки че по­ве­че­то време се използва add( ) за вмъкване на обекти, get( ) за измъкване по един и elements( ) за да се получи Iterator към редицата, има също и други методи, кои­то могат да бъдат полезни. Както обикновено с Java библиотеките, ние няма да ги използваме или да говорим за всичките, но трябва да погледнете в елек­трон­ната документация за да получите представа какво могат.

Скапване на Java


Стандартните колекции в Java съдържат метод toString( ) така че могат да про­из­ведат String представяне на самите себе си, включително съдържаните в тях обек­ти. Вътре в ArrayList, например, toString( )-ът върви по елементите на ArrayList и вика toString( ) за всеки. Да речем че искате да изведете адреса на ва­шия клас. Изглежда смислено просто да се използва this (C++ програмистите в частност са предразположени към този начин):

//: c08:CrashJava.java

// One way to crash Java

import java.util.*;


public class CrashJava {

public String toString() {

return "CrashJava address: " + this + "\n";

}

public static void main(String[] args) {



ArrayList v = new ArrayList();

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

v.add(new CrashJava());

System.out.println(v);

}

} ///:~


Оказва се че ако просто създадете CrashJava обект и го изведете, ще получите без­крайна редица от изключения. Ако обаче сложите обектите CrashJava в ArrayList и го изведете този ArrayList както е показано тук, той не може да се спра­ви и не получавате даже изключение; Java просто се скапва. (Но най-мал­ко­то не повлича и операционната система.) Това беше тествано с Java 1.1.

Това което става е автоматично превръщане към Stringове. Като напишете:

"CrashJava address: " + this

компилаторът вижда String следван от ‘+’ и нещо, което не е String, така че се опит­ва да превърне this в String. Прави това чрез извикване на toString( ), кое­то дава рекурсивно извикване. Когато това се случи вътре в ArrayList, изглежда че стекът се препълва преди механизмът за следенето му да има време да се на­ме­си.

Ако наистина искате да изведете адреса на обекта в подобен случай, решението е да извикате Object toString( ) метода, който прави точно това. Така че на­мес­то this ще напишете super.toString( ). (Това работи само ако направо сте на­сле­ди­ли от Object или никой от родителските класове не е подтиснал метода toString( )).

BitSet


BitSet е в действителност Vector за битове и се използва за обработка на много ин­формация от типа "включено-изключено". Той е ефективен само от гледна точ­ка на дължината; ако ви трябва ефикасен достъп трябва да знаете че е малко по-бавен от случая с обикновените даннови типове.

В добавка минималната дължина на BitSet е дължината на long: 64 бита. Това зна­чи че ако работите с нещо по-малко, да кажем 8 бита, BitSet ще бъде малко пра­хоснически, така че е по-добре да създадете собствен клас за работа с ва­ши­те флагове.

В нормален Vector колекцията ще се разширява с добавянето на нови елементи. BitSet също прави това – някак си. Тоест понякога го прави, а понякога не, кое­то прави да изглежда, че реализацията на BitSet в Java версия 1.0 просто е лошо на­правена. (Това е оправено в Java 1.1.) Следващият пример показва как работи BitSet и демонстрира бъга на версия 1.0:

//: c08:Bits.java

// Demonstration of BitSet

import java.util.*;


public class Bits {

public static void main(String[] args) {

Random rand = new Random();

// Take the LSB of nextInt():

byte bt = (byte)rand.nextInt();

BitSet bb = new BitSet();

for(int i = 7; i >=0; i--)

if(((1 << i) & bt) != 0)

bb.set(i);

else


bb.clear(i);

System.out.println("byte value: " + bt);

printBitSet(bb);
short st = (short)rand.nextInt();

BitSet bs = new BitSet();

for(int i = 15; i >=0; i--)

if(((1 << i) & st) != 0)

bs.set(i);

else


bs.clear(i);

System.out.println("short value: " + st);

printBitSet(bs);
int it = rand.nextInt();

BitSet bi = new BitSet();

for(int i = 31; i >=0; i--)

if(((1 << i) & it) != 0)

bi.set(i);

else


bi.clear(i);

System.out.println("int value: " + it);

printBitSet(bi);
// Test bitsets >= 64 bits:

BitSet b127 = new BitSet();

b127.set(127);

System.out.println("set bit 127: " + b127);

BitSet b255 = new BitSet(65);

b255.set(255);

System.out.println("set bit 255: " + b255);

BitSet b1023 = new BitSet(512);

// Without the following, an exception is thrown

// in the Java 1.0 implementation of BitSet:

// b1023.set(1023);

b1023.set(1024);

System.out.println("set bit 1023: " + b1023);

}

static void printBitSet(BitSet b) {



System.out.println("bits: " + b);

String bbits = new String();

for(int j = 0; j < b.size() ; j++)

bbits += (b.get(j) ? "1" : "0");

System.out.println("bit pattern: " + bbits);

}

} ///:~



За случайното формиране на byte, short и int се използва генератор на слу­чай­ни числа и всяко се трансформира в съответния битов низ в BitSet. Това работи чу­десно понеже BitSet е 64 бита, така че никое не причинява нарастване на дължината. Но в Java 1.0, когато BitSet е повече от 64 бита, се проявява странно по­ведение. Ако сетнете бит който е с едно след алокираната от BitSet в мо­мен­та памет, той ще се разшири весело. Но ако се опитате да сетнете битове които не са точно до границата, ще получите изключение, понеже BitSet няма да се раз­шири правилно в Java 1.0. Примерът показва BitSet от 512 бита. Кон­струк­то­рът алокира памет за два пъти повече битове. Ако се опитате да сетнете бит 1024 или по-голям номер без първо да сетнете бит 1023, ще изхвърлите из­клю­че­ние в Java 1.0. За щастие това е оправено в Java 1.1, но избягвайте из­полз­ва­не­то на BitSet ако пишете код за Java 1.0.

Stack


Stack понякога се споменава като “last-in, first-out” (LIFO) колекция. Тоест ка­кво­­то сте “вкарали” в Stackа последно първо го “изкарвате”. Както във всички дру­ги колекции в Java, това което се вкарва и изкарва са Objectи, така че трябва да преобразувате което изкарвате.

Лошото е че наместо да се използва Vector като градивен блок за изграждане на Stack, Stack е наследен от Vector. Така че има всичкото поведение и ха­рак­те­ри­сти­ки на Vector плюс някои допълнителни черти на Stack. Трудно е да се каже да­ли проектантите са намерили точно този начин за необходим да се направят не­щата или това просто е наивно проектирано.

Ето проста демонстрация на Stack която чете ред по ред от масив и ги пъха във вид на String:

//: c08:Stacks.java

// Demonstration of Stack Class

import java.util.*;


public class Stacks {

static String[] months = {

"January", "February", "March", "April",

"May", "June", "July", "August", "September",

"October", "November", "December" };

public static void main(String[] args) {

Stack stk = new Stack();

for(int i = 0; i < months.length; i++)

stk.push(months[i] + " ");

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

// Treating a stack as a Vector:

stk.addElement("The last line");

System.out.println(

"element 5 = " + stk.elementAt(5));

System.out.println("popping elements:");

while(!stk.empty())

System.out.println(stk.pop());

}

} ///:~



Всеки ред в масива months се пъха в Stack с push( ), а по-късно се извлича от там с pop( ). За да се подчертаят, операции на Vector също се изпълняват със Stack обект. Това е възможно поради факта, че чрез наследяването Stack е Vector. Така всички операции които може да се изпълнят с Vector могат също да се изпълнят със Stack, като например elementAt( ).

Map


Vector позволява да се избира от последователността посредством номер, така че може да се асоциира с опеделено количество обекти. Какво обаче ще стане, ако искате да изберете измежду обектите по някакъв друг критерий? Stack е един такъв пример: критерият при него е “това нещо което последно е влязло в сте­ка.” Мощно развитие на идеята за “избор от последователност” е ал­тер­на­тив­но наричано карта, a речник или асоциативно поле. Концептуално тя при­ли­ча на Вектора, но наместо да избирате обект по номер го избирате чрез друг обект! Това често е ключов процес в програмите.

Концепцията се появява в Java като abstractния клас Dictionary. Интерфейсът му е праволинеен: size( ) казва колко елемента има вътре, isEmpty( ) е true ако ня­ма елементи, put(Object key, Object value) добавя стойност (нещото което ис­кате), асоциира го с ключ (нещото чрез което ще търсите). get(Object key) да­ва стойността съответстваща на определен ключ, а remove(Object key) маха двой­ката ключ-стойност от списъка. Има Итератори: keys( ) дава Iterator за клю­човете, а elements( ) дава Iterator за всички стойности. Това е всичко за Dictionary.



Dictionary не е ужасяващо труден за прилагане. Ето един прост подход, който из­ползва два Vectorа, един за ключовете и един за стойностите:

//: c08:AssocArray.java

// Simple version of a Map

import java.util.*;


public class AssocArray extends AbstractMap {

private ArrayList keys = new ArrayList();

private ArrayList values = new ArrayList();

public int size() { return keys.size(); }

public boolean isEmpty() {

return keys.isEmpty();

}

public Object put(Object key, Object value) {



int index = keys.indexOf(key);

if (index == -1) { // Key not found

keys.add(key);

values.add(value);

return null;

} else { // Key already in table; replace

Object returnval = values.get(index);

values.set(index, value);

return returnval;

}

}



public Object get(Object key) {

int index = keys.indexOf(key);

// indexOf() Returns -1 if key not found:

if(index == -1) return null;

return values.get(index);

}

public Object remove(Object key) {



int index = keys.indexOf(key);

if(index == -1) return null;

keys.remove(index);

Object returnval = values.get(index);

values.remove(index);

return returnval;

}

public Set keySet() {



return new HashSet(keys);

}

public Collection values() {



return values;

}

public Set entrySet() {



Set set = new HashSet();

// Iterator it = keys.iterator();

// while(it.hasNext()) {

// Object k = it.next();

// Object v = values.get(values.indexOf(k));

// set.add(new Map.Entry(k, v));

// }

return set;



}

// Test it:

public static void main(String[] args) {

AssocArray aa = new AssocArray();

for(char c = 'a'; c <= 'z'; c++)

aa.put(String.valueOf(c),

String.valueOf(c)

.toUpperCase());

char[] ca = { 'a', 'e', 'i', 'o', 'u' };

for(int i = 0; i < ca.length; i++)

System.out.println("Uppercase: " +

aa.get(String.valueOf(ca[i])));

}

} ///:~


Първото нещо което личи в дефиницията на AssocArray е че той extends Dictionary. Това значи че AssocArray е от типа Dictionary, така че може да му по­ръчвате същите неща като на Dictionary. Ако направите собствен Dictionary, как­то е сторено тук, всичко което трябва да се направи е да се попълнят ме­то­ди­те в Dictionary. (И трябва да подтиснете всички методи, понеже всички са – с изключение на конструктора – абстрактни.)

Vectorите keys и values са свързани с общ индексен номер. Тоест ако извикате put( ) с ключ “roof” и стойност “blue” (предполага се, че асоциирате различните ча­сти на къща с цветовете, в които те са боядисани) и вече има 100 елемента в AssocArray, тогава “roof” ще бъде 101-я елемент на keys и “blue” ще бъде 101 еле­мент на values. Ако погледнете get( ) когато му давате “roof” като ключ, той произ­вежда индексен номер с keys.indexOf( ), а после дава стойността чрез взе­ма­не по индексния номер от вектора values.

Тестът в main( ) е прост; това е карта (в тази секция "карта" има смисъл на "съ­от­­ветствие" -бел.пр.) на малки букви към големи, което очевидно би могло да се направи по много други по-ефективни начини. Но тук показва че AssocArray е работоспособен.

Стан­дартният Java съдържа два различни типа Mapове: HashMap и TreeMap. И двата имат за интерфейс HashMap (понеже и двата реализират Map), но се раз­личават по нещо съществено: ефективността. Ако гледате какво прави get( ), до­ста бавно е да се търси през ArrayList за ключа. Това е мястото където HashMap ускорява нещата. Вместо тъпото линейно търсене се използва hash code. Наш кодът е начин да се вземе нещо от ключа и да се направи “от­но­си­тел­но уникален” int за обекта на ключа. Всички обекти имат наш код, а hashCode( ) е метод в кореновия клас Object. HashMap взима hashCode( ) за обект и го използва за бърз лов на ключа. Това резултира в драматично по­до­бре­ние на производителността.2 Начинът по който HashMap работи е извън об­хвата на тази книга3 – всичко което трябва да знаете е че HashMap е бърз Dictionary и че Dictionary е полезен инструмент.

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

//: c08:Statistics.java

// Simple demonstration of HashMap

import java.util.*;
class Counter {

int i = 1;

public String toString() {

return Integer.toString(i);

}

}
class Statistics {



public static void main(String[] args) {

HashMap ht = new HashMap();

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

// Produce a number between 0 and 20:

Integer r =

new Integer((int)(Math.random() * 20));

if(ht.containsKey(r))

((Counter)ht.get(r)).i++;

else

ht.put(r, new Counter());



}

System.out.println(ht);

}

} ///:~


В main( ) когато се произведе число се обвива в Integer така че манипулаторът да може да се използва с HashMap. (Не може да се използва примитив с ко­лек­ция, само манипулатор на обект.) Методът containsKey( ) методът проверява да­ли този ключ не е вече в колекцията. (Тоест, числото е излизало вече?) Ако е та­ка методът get( ) методът взема асоциирана стойност за ключа, която в слу­чая е Counter обект. Стойността i вътре в обекта се инкрементира за да инди­ци­ра, че още веднъж тази стойност е излизала.

Ако ключът не е намиран още методът put( ) ще сложи нова двойка ключ-стой­ност в HashMap. Понеже Counter автоматично инициализира стойността на i с еди­ница когато се създава, това дава и първата поява на числото.

Методът на HashMap toString( ) се движи през всички двойки ключ-стойност и из­пълнява toString( ) за всяка. Integer toString( ) е по начало дефиниран, така че може да погледнете toString( ) за Counter. Изходът от едно пускане (с малко ре­дак­тиране за прегледност) е:

{19=526, 18=533, 17=460, 16=513, 15=521, 14=495,

13=512, 12=483, 11=488, 10=487, 9=514, 8=523,

7=497, 6=487, 5=480, 4=489, 3=509, 2=503, 1=475,

0=505}

Може да се зачудите дали е необходим класът Counter който сякаш няма и функ­ционалността на обгръщащия клас Integer. Защо не използваме int или Integer? Не може да се използва int понеже всичките колекции работят само с ма­нипулатори към обекти Object. След срещата с колекциите обгръщащите кла­сове могат да придобият по-голям смисъл за вас, понеже не може да се ра­бо­ти с примитиви в колекциите. Обаче единственото нещо което можете да пра­вите с обхващащите класове на Java е да ги инициализирате с някаква стой­ност и след това да я прочитате. Тоест няма начин да се промени стойността след като е създаден обектът. Това прави Integer направо безполезен за ре­ша­ва­не на нашия проблем, така че се налага да направим нов клас за да си удо­вле­тво­рим нуждите.


Създаване на “ключови” класове


В предишния пример стандартен библиотечен клас (Integer) бе използван като ключ за HashMap. Той работеше добре като ключ, понеже е снабден с всичко необ­ходимо за това. Има капан обаче когато се използва HashMapове когато съз­давате собствени класове за използване като ключове. Например да вземем си­стема за предсказване на времето която съпоставя Groundhog обекти с Prediction обекти. Изглежда много праволинейно: създавате двата класа и из­полз­вате Groundhog като ключ и Prediction като стойност:

//: c08:SpringDetector.java

// Изглежда правдоподобно, но не работи добре.

import java.util.*;


class Groundhog {

int ghNumber;

Groundhog(int n) { ghNumber = n; }

}
class Prediction {

boolean shadow = Math.random() > 0.5;

public String toString() {

if(shadow)

return "Six more weeks of Winter!";

else

return "Early Spring!";



}

}
public class SpringDetector {

public static void main(String[] args) {

HashMap ht = new HashMap();

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

ht.put(new Groundhog(i), new Prediction());

System.out.println("ht = " + ht + "\n");

System.out.println(

"Looking up prediction for groundhog #3:");

Groundhog gh = new Groundhog(3);

if(ht.containsKey(gh))

System.out.println((Prediction)ht.get(gh));

}

} ///:~


Всеки Groundhog номер за идентичност, така че може да търсите Prediction в HashMap като кажете “Дай ми Predictionа асоцииран с Groundhog номер 3.” Кла­сът Prediction съдържа boolean което се инициализира чрез Math.random( ) и toString( ) което интерпретира резултата. В main( ) HashMap се попълва с Groundhogове и асоциираните им Predictionи. HashMap се извежда така че да ви­дите с какво е бил запълнен. Тогава Groundhog с номер за идентичност 3 се из­­ползва за намиране на предсказание с Groundhog номер 3.

Изглежда доста просто, но не работи. Проблемът е в това, че Groundhog е на­сле­дено от общия коренов клас Object (което се случва като не споменете базов клас, така всички класове непременно наследяват Object). Използва се метода на Object hashCode( ) за генерация на хаш код за всеки обект, а по под­раз­би­ра­не за тази цел се взема адреса на обекта. Така първият екземпляр на Groundhog(3) не дава хаш код като втория екземпляр на Groundhog(3) който се опитахме да използваме за намирането.

Може да изглежда, че всичко което трябва да се направи е да се напише код за под­тискането на hashCode( ). Но и така няма да работи докато не направите още нещо: да подтиснете equals( ) което също е част от Object. Този метод се из­ползва от HashMap когато се опитвате да определите дали един ключ е равен на друг ключ. По-нататък, Object.equals( ) просто сравнява адресите на обекти, та­ка че единият Groundhog(3) не е равен на другия Groundhog(3).

Вижда се че за да се използват собствени класове за ключове в HashMap тряб­ва да се подтиснат както hashCode( ) така и equals( ), както е показано в след­но­то решение за програмата:

//: c08:SpringDetector2.java

// If you create a class that's used as a key in

// a HashMap, you must override hashCode()

// and equals().

import java.util.*;
class Groundhog2 {

int ghNumber;

Groundhog2(int n) { ghNumber = n; }

public int hashCode() { return ghNumber; }

public boolean equals(Object o) {

return (o instanceof Groundhog2)

&& (ghNumber == ((Groundhog2)o).ghNumber);

}

}


public class SpringDetector2 {

public static void main(String[] args) {

HashMap ht = new HashMap();

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

ht.put(new Groundhog2(i),new Prediction());

System.out.println("ht = " + ht + "\n");

System.out.println(

"Looking up prediction for groundhog #3:");

Groundhog2 gh = new Groundhog2(3);

if(ht.containsKey(gh))

System.out.println((Prediction)ht.get(gh));

}

} ///:~



Забележете че се използва класа Prediction от предишния пример, та SpringDetector.java трябва да се компилира първо иначе ще се получи грешка по време на компилацията на SpringDetector2.java.

Groundhog2.hashCode( ) връща ground hog номера като идентификатор. (В то­зи пример програмистът е отговорен да осигури че няма два с един и същ иден­ти­фикационен номер.) hashCode( ) не е необходимо за връщане на уникален иден­тификатор, но метода equals( ) трябва да е в състояние твърдо да установи да­ли два обекта са равни.

Въпреки че наглед equals( ) метода само проверява дали аргументът е екзем­пляр на Groundhog2 (използвайки ключовата дума instanceof която е напълно опи­сана в глава 11), instanceof фактически прави тихичко втора санитарна про­вер­ка дали обектът е null, понеже instanceof дава false ако левият аргумент е null. Предполагайки че правилният тип не е null, сравняването става по ghNumbers. Този път, като пуснете програмата, ще видите че тя дава коректен из­ход. (Много от клясовете в библиотеките на Java подтискат hashCode( ) и equals( ) методите за работа с конкретното съдържание на библиотеките.)


Properties: тип HashMap


В първия пример в тази книга беше използван един тип HashMap, а именно Properties. Във въпросния пример редовете:

Properties p = System.getProperties();

p.list(System.out);

извикваха static метода getProperties( ) да вземе специален Properties който опис­ваше характеристиките на системата. Метода list( ) е метод на Properties кой­то изпраща съдържанието към който и да е избран изходен поток. Има също save( ) метод за да се запише списък от характеристики във файл така че по-къс­но да бъде интерпретиран от load( ) метода.

Въпреки че класът Properties е наследен от HashMap, той също съдържа втори HashMap който владее обектите с характеристики “по подразбиране”. Така че ако дадена характеристика не е намерена в основния лист, потърсва се такава по подразбиране.

Класът Properties може също да се използва във вашите програми (примерът ClassScanner.java в глава 17). Може да намерите повече подробности в до­ку­мен­тацията на Java библиотеката.


Пак енумератори


Сега може да демонстрираме истинската мощ на Iterator: способността да се проу­чи последователност без да се засяга структурата на тази по­сле­до­ва­тел­ност. В следващия пример класът PrintData използва Iterator за придвижване през последователността и извикване на toString( ) метода за всеки обект. Съз­да­дени са два различни типа колекции, Vector иHashMap, те са попълнени с, рес­пективно, Mouse и Hamster обекти. (Тези класове са дефинирани по-рано в та­зи глава; забележете че трябва да сте компилирали HamsterMaze.java и WorksAnyway.java за да може да се компилира следващата програма.) Понеже Iterator скрива структурата на подлежащата колекция, PrintData не трябва да знае или да се грижи от каква колекция идва Iterator:

//: c08:Iterators2.java

// Revisiting Iterators

import java.util.*;


class PrintData {

static void print(Iterator e) {

while(e.hasNext())

System.out.println(

e.next().toString());

}

}


class Enumerators2 {

public static void main(String[] args) {

ArrayList v = new ArrayList();

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

v.add(new Mouse(i));
HashMap h = new HashMap();

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

h.put(new Integer(i), new Hamster(i));
System.out.println("ArrayList");

PrintData.print(v.iterator());

System.out.println("HashMap");

PrintData.print(h.entrySet().iterator());

}

} ///:~


Забележете че PrintData.print( ) се възролзва от факта че обектите в ко­лек­ции­те са от типа Object така че може да се вика toString( ). По-вероятно е във ва­шия решаван проблем да направите предположението че Iterator се разхожда из колекция от някакъв определен тип. Например бижте могли да пред­по­ло­жи­те че всичко е Shape с метод draw( ). Тогава трябва да направите даункаст от Object така че връщанията от Iterator.next() да дават Shape.




Сподели с приятели:
1   ...   43   44   45   46   47   48   49   50   ...   73




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

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