Едни от нещата липсващи в библиотеките на Java 1.0 и 1.1 са алгоритмичните операции, даже простото сортиране. Така че има смисъл да се създаде ArrayList който да може да сортира себе си чрез класическия Quicksort.
Един проблем при писането на родов код е, че сортирането е специфично за типа на сортираните неща. Разбира се, един подход е да се напише отделен метод за сортиране за всеки отделен тип, но би трябвало вече да можете да кажете, че това няма да доведе до лесен за повторно използване с други типове код.
Основна задача на проектирането на програми е да се “разделят нещата които се променят от тези които остават същите,” а тука кодът който остава същия е общия сортиращ алгоритъм, променят се нещата, които има да се сортират. Така че вместо да се кодира твърдо алгоритъмът в множество сортиращи програми ще се използва техниката на callback. С техниката на обратното извикване кодът който се променя се запечатва (капсулире) в негов собствен клас, а частта от кода която остава същата го вика обратно при нужда. По този начин може да се направят различни обекти да изразяват различни начини на кодиране, а да се захравят от един и същ код.
Следващият interface описва как да се сравняват два обекта, капсулирайки с това “нещата които се променят” за този конкретен проблем:
//: c08:Compare.java
// Interface for sorting callback:
package c08;
interface Compare {
boolean lessThan(Object lhs, Object rhs);
boolean lessThanOrEqual(Object lhs, Object rhs);
} ///:~
За двата метода lhs представя “левия” обект и rhs представя “десния” обект при сравняването.
Един подклас на ArrayList може да създадем който да реализира Quicksort чрез Compare. Алгоритъмът, който е известен заради скоростта си, няма да бъде обясняван тук. За подробности виж Practical Algorithms for Programmers, от Binstock & Rex, Addison-Wesley 1995.
//: c08:SortList.java
// A generic sorting list
package c08;
import java.util.*;
public class SortList extends ArrayList {
private Compare compare; // To hold the callback
public SortList(Compare comp) {
compare = comp;
}
public void sort() {
quickSort(0, size() - 1);
}
private void quickSort(int left, int right) {
if(right > left) {
Object o1 = get(right);
int i = left - 1;
int j = right;
while(true) {
while(compare.lessThan(
get(++i), o1))
;
while(j > 0)
if(compare.lessThanOrEqual(
get(--j), o1))
break; // out of while
if(i >= j) break;
swap(i, j);
}
swap(i , right);
quickSort(left, i-1);
quickSort(i+1, right);
}
}
private void swap(int loc1, int loc2) {
Object tmp = get(loc1);
set(loc1, get(loc2));
set(loc2, tmp);
}
} ///:~
Сега може да се види основанието за термина “callback,” щом методът quickSort( ) “извиква обратно” методите в Compare. Може също да видите как тази техника дава родов, лесен за повторно използване код.
За да се използва SortList трябва да се създаде клас реализиращ Compare за вида обекти които ще сортирате. Това е място където вътрешен клас не е основното нещо, но може да подобри организацията на кода. Ето пример за String обекти:
//: c08:StringSortTest.java
// Testing the generic sorting ArrayList
package c08;
import java.util.*;
public class StringSortTest {
static class StringCompare implements Compare {
public boolean lessThan(Object l, Object r) {
return ((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) < 0;
}
public boolean
lessThanOrEqual(Object l, Object r) {
return ((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) <= 0;
}
}
public static void main(String[] args) {
SortList sv =
new SortList(new StringCompare());
sv.add("d");
sv.add("A");
sv.add("C");
sv.add("c");
sv.add("b");
sv.add("B");
sv.add("D");
sv.add("a");
sv.sort();
Iterator e = sv.iterator();
while(e.hasNext())
System.out.println(e.next());
}
} ///:~
Вътрешният клас е static понеже не е необходима връзка с външния клас за да работи.
Може да се види как, щом веднъж е създадена работнагта рамка, лесно може да се използва повторно код като този – просто пишете клас който капсулира “нещата които се променят” и давате обекта на SortList.
Сравнението прави стринговете в долен регистър, така че голямо A се нарежда до малко a и не заема някакво отделно място. Този пример показва, обаче, малка недостатъчност на този подход, а именно нарежда малките и големи разновидности на една буква по реда на появяването им: A a b B c C d D. Това обикновено не е голям проблем, понеже обикновено се работи с по-дълги стрингове и ефектът не проличава. (Колекциите на Java 2 дава функционалност на стринговете която решава този проблем.)
Наследяването (extends) е използвано тука за създаване на нов тип от ArrayList – тоест, SortList е ArrayList с добавена функционалност. Използването на наследяване тук е мощен метод, но поражда проблеми. Оказва се, че някои методи са final (описано в глава 7), токо че не може да се подтиснат. Ако искате да създадете сортиран ArrayList който приема и поражда само String обекти удряте на камък, понеже add( ) и elementAt( ) са final, а точно те са методите които трябва да се подтиснат, за да приемат само String обекти. Няма късмет тука.
От друга страна, да видим с композиция: слагането на обект вътре в нов клас. Наместо да пренаписваме горния код за да изпълним това, може просто да използваме SortList вътре в новия клас. В този случай вътрешният клас за реализация на Compare ще бъде създаден анонимно:
//: c08:StrSortList.java
// Automatically sorted ArrayList that
// accepts and produces only Strings
package c08;
import java.util.*;
public class StrSortList {
private SortList v = new SortList(
// Anonymous inner class:
new Compare() {
public boolean
lessThan(Object l, Object r) {
return
((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) < 0;
}
public boolean
lessThanOrEqual(Object l, Object r) {
return
((String)l).toLowerCase().compareTo(
((String)r).toLowerCase()) <= 0;
}
}
);
private boolean sorted = false;
public void add(String s) {
v.add(s);
sorted = false;
}
public String get(int index) {
if(!sorted) {
v.sort();
sorted = true;
}
return (String)v.get(index);
}
public Iterator iterator() {
if(!sorted) {
v.sort();
sorted = true;
}
return v.iterator();
}
// Test it:
public static void main(String[] args) {
StrSortList sv = new StrSortList();
sv.add("d");
sv.add("A");
sv.add("C");
sv.add("c");
sv.add("b");
sv.add("B");
sv.add("D");
sv.add("a");
Iterator e = sv.iterator();
while(e.hasNext())
System.out.println(e.next());
}
} ///:~
Това бързичко използва пак кода от SortList за да се създаде исканата функционалност. Обаче не всичките public методи от SortList и ArrayList се появяват в StrSortList. Когато използваме код по този начин може да направим дефиниция за всеки в новия клас, а може и да започнем с няколко и периодично да се връщаме и да добавяме нови. В края на краищата новият клас ще улегне.
Предимството на този дизайн е че ще взема само String обекти и ще произвежда само String обекти и проверката става по време на компилация а не по време на изпълнението. Разбира се това се отнася само за add( ) и elementAt( ); elements( ) все още дава Iterator който няма определен тип по време на компилация. Проверката на типа за Iterator и в StrSortList се прави, разбира се, това става по време на изпълнение и се изхвърля изключение ако нещо не е наред. Това е цената: установявате ли нещо със сигурност по време на компилация или вероятно по време на изпълнение? (Тоест, “вероятно не когато тествате кода” и “вероятно когато потребителят прави нещо, с което не сте тествали.”) Като имате възможностите за избор и суматохата, по-лесно е да използвате наследяване и да издържите на кастинга – още веднъж, ако параметризирани типове се добавят към Java някой ден те ще решат този проблем.
Може да се види че в този клас има флаг наречен sorted. Бихте могли да сортирате ArrayList всеки път когато се вика add( ) и той винаги ще се поддържа сортиран. Но обикновено се добавят множество елементи към ArrayList преди започване на четенето му. Така сортирането след всеки add( ) ще бъде по-малко ефективно отколкото дза се изчака някой да иска да чете ArrayList и тога ва да се сортира, както е направено тук. Техниката на отлагане на даден процес докато той стане абсолютно необходим се нарича мързеливо изчисление. (Има аналогична техника наречена мързелива инициализация която чака дадено поле да стане необходимо за използване, та тогава го инициализира.)
Сподели с приятели: |