Ключовата дума final има малко различно значение в зависимост от контекста, но изобщо тя значи “Това не може да се променя.” Може да искате да предотвратите промените по две причини: дизайн и ефективност. Понеже тези две неща са доста различни, възможно е да се използва думата final неправилно.
Следващите секции разглеждат трите места където final може да се използва: за данни, методи и за клас.
Final данни
Много програмни езици имат начин да се заяви, че определени данни са “константи.” Константата е полезна по две причини:
-
Може да бъде константа по време на компилация която никога няма да се промени.
-
Може да бъде инициализирана по време на изпълнение и да не искате да се променя.
В случая на константа по време на компилация компилаторът може да използва константата навсякъде където трябва; тоест изчисленията се правят (веднъж -б.пр.) по време на компилация и не се правят допълнителни разходи по време на изпълнение. В Java този вид константи трябва да са примитиви и се отбелязват с ключовата дума final. Стойност трябва да се даде в момента на дефинирането на такава константа.
Поле което е и static и final има единствена частица от паметта, която не може да се променя.
Когато се използва final с обекти наместо с примитивни типове работата става малко смущаваща. С примитив final прави стойността константа, но с обектов манипулатор, final прави манипулатора константа. Манипулаторът трябва да бъде инициализиран с обект в момента на декларацията и манипулаторът никога не може да се промени да сочи друг обект. Обаче обектът може да се променя; Java не дава начин да се направи някой произволен обект константен. (Може да напишете, обаче, клас, чиито обекти ефективно са константни.) Това ограничение включва масивите, които са също обекти.
Ето пример, който демонстрира полета final:
//: c06:FinalData.java
// The effect of final on fields
class Value {
int i = 1;
}
public class FinalData {
// Can be compile-time constants
final int i1 = 9;
static final int I2 = 99;
// Typical public constant:
public static final int I3 = 39;
// Cannot be compile-time constants:
final int i4 = (int)(Math.random()*20);
static final int i5 = (int)(Math.random()*20);
Value v1 = new Value();
final Value v2 = new Value();
static final Value v3 = new Value();
//! final Value v4; // Pre-Java 1.1 Error:
// no initializer
// Arrays:
final int[] a = { 1, 2, 3, 4, 5, 6 };
public void print(String id) {
System.out.println(
id + ": " + "i4 = " + i4 +
", i5 = " + i5);
}
public static void main(String[] args) {
FinalData fd1 = new FinalData();
//! fd1.i1++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(); // Error: Can't
//! fd1.v3 = new Value(); // change handle
//! fd1.a = new int[3];
fd1.print("fd1");
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData();
fd1.print("fd1");
fd2.print("fd2");
}
} ///:~
Тъй като i1 и I2 са final примитиви със стойности известни по време на компилацията, те могат да се третират като константи по време на компилацията и не са различни по никакъв съществен начин. I3 е по-типичен случай на дефиниране на такива констранти: public за да бъдат видими извън пакета, static за да се подчертае че е единствена и final за това че е константа. Забележете че final static примитиви с константни начални стойности (т.е. константи по време на компилация) имат имена само от гоеми букви по конвенция. Също забележете че i5 не може да бъде известно по време на компилация, така че името не е с големи букви.
Че нещо е final не значи непременно, че стойността му е известна по време на компилация. Това е демонстрирано чрез инициализирането на i4 и i5 по време на изпълнение чрез използване на генератор на случайни числа. Тази част на примера също показва разликата между правенето final стойност static и не-static. Тази разлика се проявява само при инициализация по време на изпълнение, понеже константите по време на компилация се третират еднакво от компилатора. (И предполагаемо се оптимизират предварително.) Разликата се вижда от изхода на програмата:
fd1: i4 = 15, i5 = 9
Creating new FinalData
fd1: i4 = 15, i5 = 9
fd2: i4 = 10, i5 = 9
Забележете че стойностите на i4 за fd1 и fd2 са уникални, но стойността за i5 не е променена при създаването на втори FinalData обект. Това е защото е static и се инициализира веднъж при натоварването, а не всеки път при създаването на обект.
Променливите v1 до v4 демонстрират какво значи final манипулатор. Както може да видите в main( ), само защото v2 е final не означава, че не можете да промените стойността му. Не може обаче да пресвържете v2 към нов обект точно защото е final. Това е, което final значи за манипулатор. Може да видите, че същото важи и за масив, който просто е друг тип манипулатор. (Не знам начин да направя самите манипулатори на масиви final.) Правенето на манипулатори final изглежда по-малко полезно от равенето на примитиви final.
Празни final
Java 1.1 позволява създаването на blank finals, които са полета декларирани като final но не им се дава инициализираща стойност. Във всички случаи те трябва да бъдат инициализирани преди да бъдат използвани и компилаторът осигурява това. Те дават много по-голяма гъвкавост в използването на ключовата дума final понеже, например, final поле в клас може сега да бъди различно за всеки обект и все пак да запази качеството си на "финален". Ето пример:
//: c06:BlankFinal.java
// "Blank" final data members
class Poppet { }
class BlankFinal {
final int i = 0; // Initialized final
final int j; // Blank final
final Poppet p; // Blank final handle
// Blank finals MUST be initialized
// in the constructor:
BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet();
}
BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet();
}
public static void main(String[] args) {
BlankFinal bf = new BlankFinal();
}
} ///:~
Принудени сте да дадете стойност на този род членове или по време на декларирането им, или във всеки конструктор. По този начин се гарантира, че всичко ще бъде инициализирано преди употребата.
Java 1.1 позволява да се правят аргументи final чрез декларирането им като такива в аргументния списък. Това значи че вътре в метода не може да променяте това, към което сочи манипулаторът:
//: c06:FinalArguments.java
// Using "final" with method arguments
class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
g.spin();
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
} ///:~
Забележете че може да се дава null манипулатор за аргумент който е final без компилаторът да се вайка, точно както за нефинален аргумент.
Методите f( ) и g( ) показват какво става когато примитивни аргументи са final: може да четете аргумента, но не може да го променяте.
Final методи
Има две причини за съществуването на final методи. Първата е да се “заключи” методът така че никой наследяващ клас да не може да му промени поведението. Това се прави по причини на дизайна когато искате да осигурите че ако класът се наследи методът няма да бъде подтиснат.
Втората причина е ефективността. Ако направите метод final, позволявате на компилатора да превърне извикванията му в inline извиквания. Когато компилаторът види извикване на final той може (по своя преценка) да пропусне нещата, които се правят при нормалния подход за извикване на методи (слагане на аргументите на стека, преход към кода на метода, преход обратно, почистване на стека и оправяне с върнатата стойност) и вместо тях може да сложи направо тялото на метода на мястото на извикването. Това елиминира допълнителните разходи за викането на метода. Разбира се, ако методът е голям, вашият код ще набъбне и няма да усетите никакво подобрение на скоростта, понеже съкращаването на работата е малко в сравнение с времето за изпълнение на самия метод. Предвидено е Java компилаторът да е в състояние да познае такива възможности и умно да прецени дали да направи даден final метод инлайн. Обаче е по-добре да не се надържаме на възможностите на компилатора и да правим методи final само ако са много малки или искаме явно да предотвратим възможността за подтискането им.
Всеки private в клас е имплицитно final. Понеже нямате достъп до private метод, не може да го подтиснете (ако и компилаторът не дава съобщение за грешка като се опитвате да го подтиснете, вие реално не го подтискате, а създавате нов метод). Може да добавите final спецификатора към private метод но това нищо не му дава в повече.
Final класове
Като кажете че цял клас е final (чрез предхождане на дефиницията му с ключовата дума final), заявявате че не щете да наследявате от този клас и не щете никой да го прави. С други думи, по някакви проектантски или от сигурността причини този клас никога не трябва да се променя или да се наследява от други класове. Алтернативно, може да е причината ефективността и искате работата с този клас да е толкова ефестивна, колкото е възможно.
//: c06:Jurassic.java
// Making an entire class final
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
} ///:~
Забележете че данновите членове могат да бъдат final или не по ваш избор. Същите правила важат за final за даннови членове без значение дали класът е final. Декларирането на клас като final просто предотвратява наследяването – нищо повече. Поради това предотвратяване обаче всички методи на final клас са имплицитно final, понеже няма начин да се подтиснат. Така че компилаторът има същата възможност за подобряване на ефективността, както ако ги бяхте обявили final.
Може да добавите спецификатор final към метод на final клас, но нищо повече не се задава реално.
Final предпазливост
Може да изглежда добре да направите метод final докато проектирате клас. Може да чувствате че ефективността е много важна или че никой не трябва да го променя. Понякога това е така.
Но бъдете внимателни с предположенията. Изобщо е трудно да се предвиди как даден клас ще се използва в бъдеще. Ако определите метод като final бихте премахнали възможността вашият метод да се подтисне от друг програмист само защото не сте могли да си представите използването на вашия клас по неговия начин.
Стандартната Java библиотека е добър пример за това. В частност Java 1.0/1.1 Vector класът бе много използван и щеше да бъде още по-полезен ако, в името на ефективността, всички методи не бяха направени final. Лесно е да се предвиди, че ще е полезно да може да се наследява такъв полезен и общ клас, но кой знае защо проектантите му решили да забранят това. Това е иронично по две причини. Първо, Stack е наследен от Vector, което значи че Stack е Vector, което пък не е реално истина. Второ, много от най-важните методи на Vector, такива като addElement( ) и elementAt( ) са synchronized, което както ще видите в глава 14 вкарва значителни допълнителни разходи които по всяка вероятност ще унищожат подобренията вследствие на final. Това ни кара да даваме вяра на теорията, че програмистите са винаги лоши и се мъчат да познаят къде ще има оптимизация. Особено лошо е че такова лошо проектиране е направено в стандартна библиотека и всички ние ще трябва да се оправяме с него. (За щастие библиотеката за колекциите на Java 2 заменя Vector с ArrayList, който има много по-цивилизовано поведение.)
Интересно е също да се отбележи че Hashtable, друг важен клас на стандартна библиотека, няма final методи. Както се спомена на друго място в тази книга, очевидно е че различните класове са проектирани от съвършено различни хора. (Забележете краткостта на имената в Hashtable сравнени с онези във Vector.) Това е нещо от вид, който не трябва да е очевиден за потребителите на библиотеката. Когато нещата са недомислени това прави повече работа за потребителя. (Забележете че Java 2 библиотеката за колекциите заменя Hashtable със Hashmap.)
Сподели с приятели: |