Стандартните библиотеки на 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.
Сподели с приятели: |