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



страница72/73
Дата25.07.2016
Размер13.53 Mb.
#6732
1   ...   65   66   67   68   69   70   71   72   73

Класове само за четене


Докато локалното копие произведено от 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) вместо четири.

Този подход е подходящ когато:



  1. Ви трябва неизменяем обект и

  2. Често трябва да правите много модификации или

  3. Е скъпо да правите нови неизменяеми обекти

Неизменяеми 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 и малко допълнителна магия от ком­пи­ла­то­ра.



Сподели с приятели:
1   ...   65   66   67   68   69   70   71   72   73




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

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