Да сумираме до тук: вашият пръв, най-ефективен начин за владеене на обекти е масив, а и сте принудени да използвате масив ако владяното е примитиви. До края на главата ще разгледаме по-общия случай, когато не се знае към момента на написването на програмата колко обекта ще са нужни, та трябва по-усложнен начин за владеенето на обектите. Java има три типа класове-колекции за решаване на този проблем: List, Set и Map. Изненадващо много проблеми могат да се рашат с тези три инструмента.
Сред другите им характеристики – Set, например, може да съдържа само един обект с дадена стойност, а Map е асоциативен масив което позволява да се асоциира обект с някой друг обект – е и тази, че колекторните класове на Java автоматично сами ще си променят дължината при нужда. По този начин може да се владеят произволен брой обекти и не е необходимо да се грижим за това по време на написването на програмите.
Неудобство: неизвестен тип
“Неудобството” да се използват колекциите на Java е, че когато сложите обект в тях губите информацията за типа му. Това става защото по времето на написването на класа-колекция програмистът не е имал представа за конкретния тип класове, които ще образуват колекцията, а освен това ако се направи да се поддържа само вашият тип колекцията няма вече да е инструмент за всеобща употреба. Така че колекцията съдържа манипулратори на обекти от тип Object, каквито са разбира се всички обекти в Java, понеже това е кореновият клас. (Това не включва примитивните класове, понеже те не са наследени от нищо.) Това е много добро решение, освен в следните случаи:
-
Тъй като информацията за типа се изхвърля когато сложите обект в колекцията, всякакви типове обекти могат да се сложат там, даже и да сте възнамерявали да сложите, да речем, само котки. Някой много лесно може да сложи куче в колекцията.
-
Тъй като информацията за типа е изгубена, единственото известно нещо в колекцията е че се съхранява манипулатор към Object. Трябва да се направи каст към типа треди да се използва манипулатора.
Откъм предимствата: Java не дава да се използват погрешно обектите които сте сложили в колекцията. Ако хвърлите куче в колекцията от котки, после минете през колекцията опитвайки се да третирате всичко като котки, ще получите изключение когато стигнете до кучето. В същия смисъл, ако се опитате да направите каст на манипулатор към куче да стане на котка ще получите изключение по време на изпълнение.
Ето пример:
//: c08:CatsAndDogs.java
// Simple collection example
import java.util.*;
class Cat {
private int catNumber;
Cat(int i) {
catNumber = i;
}
void print() {
System.out.println("Cat #" + catNumber);
}
}
class Dog {
private int dogNumber;
Dog(int i) {
dogNumber = i;
}
void print() {
System.out.println("Dog #" + dogNumber);
}
}
public class CatsAndDogs {
public static void main(String[] args) {
ArrayList cats = new ArrayList();
for(int i = 0; i < 7; i++)
cats.add(new Cat(i));
// Not a problem to add a dog to cats:
cats.add(new Dog(7));
for(int i = 0; i < cats.size(); i++)
((Cat)cats.get(i)).print();
// Dog is detected only at run-time
}
} ///:~
Може да се види, че използването на ArrayList е праволинейно: създавате го, слагате обекти вътре чрез add( ), а по-късно ги вадите чрез get( ). (Забележете че Vector има метод size( ) за да може да видите колко елемента са прибавени и да не стъпите неочаквано извън края, предизвиквайки изключение по време на изпълнението.)
Класовете Cat и Dog са различни – нямат нищо общо помежду си, освен че са Objects. (Ако не кажете явно от кой клас наследявате, наследявате от Object.) Класът Vector който идва от java.util, държи Objects, така че не само може да слагате Cat в тази колекция използвайки метода на Vector add( ), но може също да добавите обекти Dog без оплаквания и грешки по време на изпълнение. Когато се опитате да извадите неща които мислите че са Cat обекти използвайки метода на Vector get( ), получавате манипулатор към Object който трябва да се превърна в такъв към Cat. Тогава трябва да сложите целия израз в скоби за да стане превръщането проди да се използва методът print( ) на Cat, иначе ще получите синтактична грешка. По време на изпълнение, когато се опитате да направите каст на Dog обект към Cat, ще получите изключение.
Това е повече от просто досадно. Това е нещо, което може да доведе до трудни за откриване грешки. Ако една (или няколко) части от програма вмъкват обекти в колекция, а видите че само отделна част е изхвърлила изключение за неправилен тип на обект в колекцията, трябва да намерите къде е станало неправилното вмъкване. Това се прави чрез преглеждане на кода, който е почти най-лошия инструмент за откриване на грешки. От друга страна, удобно е да се започне с някаква стандартизирана колекция, напук на оскъдицата и тромавостта.
Понякога работи правилно при всички случаи
Понякога изглежда че нещата стават както трябва без кастинг към оригиналния тип. Първият случай е доста специален: Класът String има малко по-голяма помощ от страна на компилатора за да работи гладко. Щом компилаторът очаква String обект и не получава такъв, той автоматично ще извика метода toString( ) който е определен в Object и може да бъде подтиснат от всеки Java клас. Този метод дава желания String обект, който после се използва където е нужно.
Така всичко което трябва да направите за да извеждат вашите обекти е да подтиснете метода toString( ) както е показано в следващия пример:
//: c08:WorksAnyway.java
// In special cases, things just seem
// to work correctly.
import java.util.*;
class Mouse {
private int mouseNumber;
Mouse(int i) {
mouseNumber = i;
}
// Magic method:
public String toString() {
return "This is Mouse #" + mouseNumber;
}
void print(String msg) {
if(msg != null) System.out.println(msg);
System.out.println(
"Mouse number " + mouseNumber);
}
}
class MouseTrap {
static void caughtYa(Object m) {
Mouse mouse = (Mouse)m; // Cast from Object
mouse.print("Caught one!");
}
}
public class WorksAnyway {
public static void main(String[] args) {
ArrayList mice = new ArrayList();
for(int i = 0; i < 3; i++)
mice.add(new Mouse(i));
for(int i = 0; i < mice.size(); i++) {
// No cast necessary, automatic call
// to Object.toString():
System.out.println(
"Free mouse: " + mice.get(i));
MouseTrap.caughtYa(mice.get(i));
}
}
} ///:~
You can see the redefinition of toString( ) in Mouse. In the second for loop in main( ) you find the statement:
System.out.println("Free mouse: " + mice.get(i));
След знака ‘+’ компилаторът очаква да види String обект. get( ) дава Object, така че за да получи желания String компилаторът неявно вика toString( ). За нещастие тези магии стават само със String; няма ги за никой друг тип.
Вторият подход за скриване на кастинга е заложен в Mousetrap. Методът caughtYa( ) приема не-Mouse, но Object, който после превръща в Mouse. Това е твърде самонадеяно, разбира се, понеже чрез приемане на Object нищо не би могло да бъде подадено към метода. Ако обаче кастингът не е коректен – ако сте подали неправилен тип – ще получите изключение по време на изпълнение. Това не е толкова добре като проверката по време на компилация, но все още бива. Забележете че при използването на метода:
MouseTrap.caughtYa(mice.get(i));
Не е необходим каст.
Правене на Vector чувствителен към типа
Може да не искате да изоставите този въпрос точно сега. По-добро решение е да наследите Vector, така че да приема само вашия тип и да произвежда само вашия тип:
//: c08:GopherList.java
// A type-conscious ArrayList
import java.util.*;
class Gopher {
private int gopherNumber;
Gopher(int i) {
gopherNumber = i;
}
void print(String msg) {
if(msg != null) System.out.println(msg);
System.out.println(
"Gopher number " + gopherNumber);
}
}
class GopherTrap {
static void caughtYa(Gopher g) {
g.print("Caught one!");
}
}
class GopherList {
private ArrayList v = new ArrayList();
public void add(Gopher m) {
v.add(m);
}
public Gopher get(int index) {
return (Gopher)v.get(index);
}
public int size() { return v.size(); }
public static void main(String[] args) {
GopherList gophers = new GopherList();
for(int i = 0; i < 3; i++)
gophers.add(new Gopher(i));
for(int i = 0; i < gophers.size(); i++)
GopherTrap.caughtYa(gophers.get(i));
}
} ///:~
Това е като в предния пример, освен че новият клас GopherVector има private член от тип Vector (наследяването ат Vector има тенденция да разочарова, по причини които ще видите по-късно), и методи точно като Vector. Обаче той не приема и не произвежда родови Objectи, само Gopher обекти.
Понеже GopherVector ще приема само Gopher, ако бяхте написали:
gophers.add(new Pigeon());
щяхте да получите грешка по време на компилация. Този подход, много по-досаден от програмистка гледна точка, ще ви каже незабавно ако не използвате правилен тип.
Забележете че не е необходим каст за използване на get( ) – то си е Gopher.
Параметризирани типове
Този вид проблеми не е изолиран – има множество случаи, когато трябва да получите нови типове от налични типове, в които е необходимо да се разполага с информация за типа по време на компилация. Това е концепцията за параметризиран тип. В C++ това се поддържа директно от езика с шаблон. В определен момент Java резервира ключовата дума generic за да я използва някой ден за параметризираните типове, но не е сигурно, че това някога ще се случи.
Сподели с приятели: |