Докато локалното копие произведено от clone( ) дава желаните резултати в съответните случаи, то е и пример на заставяне на програмиста (автора на метода) да бъде отговорен за лошите ефекти на псевдонимите. Какво ще стане ако правите толкова много употребявана и обща библиотека, че не може да се знае дали винаги ще бъде клонирано където трябва? Или по-вероятно, какво ако вие искате да позволите псевдоними заради ефективността – за предотвратяване на ненужна дубликация на обекти – но не искате отрицателната страна на псевдонимите?
Едно решение е да се създаде неизменяем обект който принадлежи към класовете само за четене. Може да дефинирате така един клас, че да няма методи които да му променят вътрешното състояние. В такъв клас псевдонимите не могат да попречат понеже вътрешната структура може само да се чете, така че и много места в кода да я четат няма проблеми.
Като прост пример с неизменяеми обекти стандартната библиотека на Java съдържа “обгръщащи” класове за всички примитивни типове. Може вече да сте открили това, че ако искате да запомните int в колекция като ArrayList (която взема само Object манипулатори), може да обгърнете вашия int в класа от стандартната библиотека Integer:
//: c12:ImmutableInteger.java
// The Integer class cannot be changed
import java.util.*;
public class ImmutableInteger {
public static void main(String[] args) {
ArrayList v = new ArrayList();
for(int i = 0; i < 10; i++)
v.add(new Integer(i));
// But how do you change the int
// inside the Integer?
}
} ///:~
Класът Integer (както и всички “обгръщащи”примитиви класове) реализира непроменимост по прост начин: те нямат методи които позволяват да се променя обекта.
Ако се нуждаете от обект който съдържа примитивен тип на който да може да се променя стойността, трябва да го създадете сами. За щастие това е тривиално:
//: c12:MutableInteger.java
// A changeable wrapper class
import java.util.*;
class IntValue {
int n;
IntValue(int x) { n = x; }
public String toString() {
return Integer.toString(n);
}
}
public class MutableInteger {
public static void main(String[] args) {
ArrayList v = new ArrayList();
for(int i = 0; i < 10; i++)
v.add(new IntValue(i));
System.out.println(v);
for(int i = 0; i < v.size(); i++)
((IntValue)v.get(i)).n++;
System.out.println(v);
}
} ///:~
Забележете че n е приятелско за опростяване на кодирането.
IntValue може да е даже още по-просто ако инициализацията по подразбиране с нула се окаже подходяща (тогава не ви трябва конструктора) и не се грижите за извеждането º (тогава не се нуждаете от toString( )):
class IntValue { int n; }
Изваждането на елемент и кастингът му са малко тромави, но това е черта на ArrayList, не на IntValue.
Създаване на класове само за четене
Възможно е да създадете собствен клас само за четене. Ето пример:
//: c12:Immutable1.java
// Objects that cannot be modified
// are immune to aliasing.
public class Immutable1 {
private int data;
public Immutable1(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable1 quadruple() {
return new Immutable1(data * 4);
}
static void f(Immutable1 i1) {
Immutable1 quad = i1.quadruple();
System.out.println("i1 = " + i1.read());
System.out.println("quad = " + quad.read());
}
public static void main(String[] args) {
Immutable1 x = new Immutable1(47);
System.out.println("x = " + x.read());
f(x);
System.out.println("x = " + x.read());
}
} ///:~
Всички данни са private и виждате, че никой от public методите не променя данните. Разбира се, методът който изглежда че променя данните е quadruple( ), но това създава нов Immutable1 и оставя оригиналния незасегнат.
Методът f( ) взима Immutable1 обект и изпълнява различни операции с него, а изходът от main( ) демонстрира че x няма промяна. Така обектът на x може да има много псевдоними без вреда понеже класът Immutable1 е проектиран да осигури че обектите от него не могат да бъдат променяни.
Недостатъкът на непроменимостта
Отначало създаването на непроменим клас изглежда елегантно решение. Обаче щом се наложи промяна на стойности страдате от допълнителната работа за създаване на нов обект, както и потенциално по-честото събиране на боклука. За някои класове това не е проблем, но за други (като класа String) това е забранително скъпо.
Решението е да се създаде съпътстващ клас който може да бъде променян. В такъв случай когато правите много промени, може да се превключите към променящия компаньон и отново към непроменимия клас когато свършите.
Горният пример може да бъде променен да показва това:
//: c12:Immutable2.java
// A companion class for making changes
// to immutable objects.
class Mutable {
private int data;
public Mutable(int initVal) {
data = initVal;
}
public Mutable add(int x) {
data += x;
return this;
}
public Mutable multiply(int x) {
data *= x;
return this;
}
public Immutable2 makeImmutable2() {
return new Immutable2(data);
}
}
public class Immutable2 {
private int data;
public Immutable2(int initVal) {
data = initVal;
}
public int read() { return data; }
public boolean nonzero() { return data != 0; }
public Immutable2 add(int x) {
return new Immutable2(data + x);
}
public Immutable2 multiply(int x) {
return new Immutable2(data * x);
}
public Mutable makeMutable() {
return new Mutable(data);
}
public static Immutable2 modify1(Immutable2 y){
Immutable2 val = y.add(12);
val = val.multiply(3);
val = val.add(11);
val = val.multiply(2);
return val;
}
// This produces the same result:
public static Immutable2 modify2(Immutable2 y){
Mutable m = y.makeMutable();
m.add(12).multiply(3).add(11).multiply(2);
return m.makeImmutable2();
}
public static void main(String[] args) {
Immutable2 i2 = new Immutable2(47);
Immutable2 r1 = modify1(i2);
Immutable2 r2 = modify2(i2);
System.out.println("i2 = " + i2.read());
System.out.println("r1 = " + r1.read());
System.out.println("r2 = " + r2.read());
}
} ///:~
Immutable2 съдържа методи които, както преди, запазват неизменяемостта на обектите чрез правене на нов обект винаги щом е необходима промяна. Те са add( ) и multiply( ) методите. Съпътстващият клас е наречен Mutableи също има add( ) и multiply( ) методи, но те променят Mutable обекта наместо да правят нов. Освен това Mutable няма ветод да изпозва данните си да прави Immutable2 обект и обратно.
Вата статични метода modify1( ) и modify2( ) показват два различни подхода за получаване на един и същ резултат. В modify1( ) всичко е направено в Immutable2 и може да видите че четири нови Immutable2 са създадени при работата. (И всеки път когато val се пре-приравнява, предишният обект става боклук.)
В метода modify2( ) може да видите че първо се взема Immutable2 y и се създава Mutable от него. (Това е точно като викането на clone( ) както видяхте по-рано, но този път се създава различен тип обект.) Тогава Mutable обектът се използва за правене на много модификации без да е необходимо създаването на много нови обекти. Накрая отново се превръща в Immutable2. Тук се създават два нови обекта (Mutable и резултата Immutable2) вместо четири.
Този подход е подходящ когато:
-
Ви трябва неизменяем обект и
-
Често трябва да правите много модификации или
-
Е скъпо да правите нови неизменяеми обекти
Неизменяеми Stringове
Да видим следния код:
//: c12:Stringer.java
public class Stringer {
static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = new String("howdy");
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
} ///:~
Когато q се подава в upcase( ) това фактически е копие на манипулатора към q. Обектът сочен от този манипулатор си стои в единствено физическо място. Манипулаторите се копират при подаването.
Гледайки дефиницията на upcase( ) може да забележите че подаваният манипулатор има име s и съществува само докато тялото на upcase( ) се изпълнява. Когато upcase( ) завърши локалният манипулатор s изчезва. upcase( ) връща резултата, който е оригиналният стринг с всички знаци в горен регистър. Разбира се се връща манипулатор към резултата.Но излиза че върнатият манипулатор е за новия обект, а оригиналният q е изоставен. Как се случва това?
Неявни константи
Ако кажем:
String s = "asdf";
String x = Stringer.upcase(s);
Искаме ли наистина методът upcase( ) да промени аргумента? Изобщо, не, понеже аргументът изглежда на четящия кода като парче подадена на метода информация, не като нещо за модифициране. Това е важна гаранция, понеже прави кода по-лесен за четене и разбиране.
В C++ наличността на такава гаранция беше достатъчно важна, за да се използва специална ключова дума, const, да позволи на програмиста да гарантира че манипулаторът (указател или псевдоним в C++) няма да бъде използван за промяна на оригиналния обект. Но C++ програмистът трябваше да бъде грижлив и да използва const навсякъде. Това може да бъде смущаващо и лесно за забравяне.
Претоварване на ‘+’ и StringBuffer
Обектите от класа String са проектирани да бъдат неизменяеми, чрез показаната технология. Ако разгледате онлайн документацията за класа String (която е дадена в резюме по-късно в тази глава), ще видите че всеки метод който променя String фактически създава и връща съвсем нов String обект съдържащ модификацията. Оригиналният String остава незасегнат. Така няма черта в Java подобна на const в C++ за поддръжка от страна на компилатора на неизменяемостта на вашите обекти. Ако я искате, трябва да я направите сами, както прави String.
Понеже String обектите са неизменяеми, може един String да има колкото си искате псевдоними. Понеже е само за четене няма как един манипулатор да измени нещо за другите манипулатори. Така че обектът само за четене добре решава проблема с псевдонимите.
Изглежда също възможно да се задоволят всички случаи на промяна като се създава съвсем нов обект съдържащ я, както прави String. Обаче за някои операции това не е ефективно. Такъв случай е операторът ‘+’ който е претоварен за String обекти. Претоварването значи че се добавя допълнително значение когато се приложи за конкретен клас. ( ‘+’ и ‘+=‘ за String са единствените оператори които са претоварени в Java и Java не позволява на програмистът да претоварва които и да било други4).
Когато се използва със String обекти ‘+’ позволява да се конкатенират Stringове+:
String s = "abc" + foo + "def" + Integer.toString(47);
Бихме могли да си представим как това би могло да работи: Stringът“abc” би могъл да има метод append( ) който създава нов String обект съдържащ “abc” съединен със съдържанието на foo. Новият String обект после би създал нов String в който добавя “def” и така нататък.
Това сигурно ще работи, но то би изисквало създаването на много String обекти само за да се съединят Stringовете, а тогава ще има много междинни String обекти които трябва да се оберат като боклук. Подозирам че проектантите на Java са опитали този подход отначало (което е урок по програмно проектиране – нищо не знаете за системата докато не направите някакъв код да работи). Подозирам също че те са открили, че това води до неприемлива производителност.
Решението е променим съпътстващ клас какъвто беше вече показан. За String този съпътстващ клас е наречен StringBuffer и компилаторът автоматично създава StringBuffer за изчисляване на някои изрази, в частност когато претоварените оператори + и += се използват със String обекти. Този пример показва какво става:
//: c12:ImmutableStrings.java
// Demonstrating StringBuffer
public class ImmutableStrings {
public static void main(String[] args) {
String foo = "foo";
String s = "abc" + foo +
"def" + Integer.toString(47);
System.out.println(s);
// The "equivalent" using StringBuffer:
StringBuffer sb =
new StringBuffer("abc"); // Creates String!
sb.append(foo);
sb.append("def"); // Creates String!
sb.append(Integer.toString(47));
System.out.println(sb);
}
} ///:~
При създаването на String s компилаторът прави код грубо еквивалентен на следния използващ sb: създава се StringBuffer и се използва append( ) за добавяне на нови знаци направо към StringBuffer обекта (наместо да се прави ново копие всеки път). Докато това е по-ефективно, нищо не струва че всеки път когато създавате стринг с кавички като “abc” и “def”компилаторът ги превръща в String обекти. Така че може да има повече на брой от очакваното обекти, напук на ефективността постигната от StringBuffer.
Класовете String и StringBuffer
Ето преглед на методите достъпни в String и StringBuffer така че може да си създадете представа за начина по който си взаимодействат. Таблиците не съдържат всеки отделен метод, а само важните за тази дискусия. Методите които са претоварени са резюмирани в един ред.
Първо, класът String:
-
Метод
|
Аргументи, Претоварване
|
Употреба
|
Конструктор
|
Претоварени: Default, String, StringBuffer, char масиви, byte масиви.
|
Създаване на String обекти.
|
length( )
|
|
Брой на знаците в String.
|
charAt()
|
int Index
|
Char-ът в определено място на Stringа.
|
getChars( ), getBytes( )
|
Началото и края откъдето да се копира, масивът в който се копира, индекс в последния.
|
Копира charове или bytes във външен масив.
|
toCharArray( )
|
|
Дава char[] съдържащ знаците в Stringа.
|
equals( ), equals-IgnoreCase( )
|
String с който да се сравнява.
|
Тест за равенство на съдържанията на два Stringа.
|
compareTo( )
|
String с който да се сравнява.
|
Резултата негативен, нула или положителен в зависимост от лексикографичното подреждане на String и аргумента. Малките и големи букви не са равни!
|
regionMatches( )
|
Отместване в този String, другия String и неговото отместване и дължина за сравняване. С оверлоудинг се добавя “пренебрегване на регистъра.”
|
Булев резултат индициращ дали обхватът съвпада.
|
startsWith( )
|
String с който може да започва. Оверлоуд добавя отместване.
|
Булев резултат индициращ дали String започва с аргумента.
|
endsWith( )
|
String който може да бъде суфикс на този String.
|
Булев резултат индициращ дали е суфикс.
|
indexOf( ), lastIndexOf( )
|
Претоварени: char, char и начален индекс, String, String и начален индекс
|
Връща -1ако аргументът не е намерен в този String, иначе връща индексът където аргументът започва. lastIndexOf( ) търси отзад напред.
|
substring( )
|
Претоварени: Начален индекс, начален индекс и индекс на края.
|
Връща нов String обект съдържащ специфицилания знаков набор.
|
concat( )
|
Stringа за конкатениране
|
Връща нов String обект съдържащ знаците на оригиналния String следвани от знаците на аргумента.
|
replace( )
|
Старият знак който се търси, новия знак с който да се замени.
|
Връща нов String обект с направени замествания. Използва стария String ако не е намерено съвпадение.
|
toLowerCase( ) toUpperCase( )
|
|
Връща нов String обект с променен регистър на буквите. Изполва стария String ако не са били необходими промени.
|
trim( )
|
|
Връща нов String обект с непечатуемите знаци махнати от края. Използва стария String ако не са били необходими промени.
|
valueOf( )
|
Претоварени: Object, char[], char[] и отмествания и брой, boolean, char, int, long, float, double.
|
Връща String съдържащ знаковото представяне на аргумента.
|
intern( )
|
|
Произвежда единствен String манипулатор за всяка уникална знакова редица.
|
Може да видите че всеки метод на String грижливо връща нов String когато е необходимо да се промени съдържанието. Също когато съдържанието не се променя връща се манипулатор към оригиналния String. Това спестява памет и допълнителна работа.
Ето класа StringBuffer:
-
Метод
|
Аргументи,претоварване
|
Използване
|
Конструктор
|
Претоварени: по подразбиране, дължина на създавания буфер, String от който да се създаде.
|
Създава нов StringBuffer обект.
|
toString( )
|
|
Създава String от този StringBuffer.
|
length( )
|
|
Брой на знаците в StringBufferа.
|
capacity( )
|
|
Връща текущия брой алокирани шпации.
|
ensure-
Capacity( )
|
Цяло индициращо желания капацитет.
|
Прави StringBuffer способен да побере най-малко желаното количество знаци.
|
setLength( )
|
Цяло индициращо новата дължина на стринга в буфера.
|
Реже и разширява предишния стринг. Ако разширява, пълни с нули.
|
charAt( )
|
Цяло показващо индекса на желания елемент.
|
Връща char който е на това място в буфера.
|
setCharAt( )
|
Цяло показващо индекса на желания елемент и новата char стойност на елемента.
|
Модифицира стойността на това място.
|
getChars( )
|
Начало и край откъдето да се копира, масив в който да се копира, индекс в него.
|
Копира charове във външен масив. Няма getBytes( ) като в String.
|
append( )
|
Претоварени: Object, String, char[], char[] с отместване и дължина, boolean, char, int, long, float, double.
|
Аргументът се превръща в стринг и добавя към края на текущия буфер, увеличавайки го ако е необходимо.
|
insert( )
|
Претоварени, всеки с пръв аргумент отместването където да започне вмъкването: Object, String, char[], boolean, char, int, long, float, double.
|
Вторият аргумент се превръща в стринг и се вмъква в текущия буфер започвайки от отместването. Буферът се увеличава ако е необходимо.
|
reverse( )
|
|
Редът на знаците в буфера е обърнат.
|
Най-често използвания метод е append( ), който се използва и от компилатора за изчисляване на String изрази които съдържат ‘+’ и ‘+=‘ оператори. Методът insert( ) има подобна форма, а двата метода изпълняват значителна работа със стринговете без да създават нов обект.
Stringовете са специални
До тук видяхме че класът String не е просто още един клас в Java. Има множество специални случаи със String, не най-маловажният от които е че това е вграден клас и основен за Java. После идва фактът че стринговете в кавички се превръщат в String от компилатора и специални претоварени оператори + и +=. В тази глава видяхте и останалия специален случай: грижливо построена неизменимост с помощта на StringBuffer и малко допълнителна магия от компилатора.
Сподели с приятели: |