צור קבועים ספורים בג'אווה

קבוצה של "קבועים אינספור" היא אוסף מסודר של קבועים שניתן לספור, כמו מספרים. מאפיין זה מאפשר לך להשתמש בהם כמו מספרים כדי להוסיף אינדקס למערך, או שתוכל להשתמש בהם כמשתנה האינדקס בלולאת for. בג'אווה, אובייקטים כאלה מכונים לרוב "קבועים ספורים".

שימוש בקבועים ספורים יכול להפוך את הקוד לקריא יותר. לדוגמה, ייתכן שתרצה להגדיר סוג נתונים חדש בשם צבע עם קבועי אדום, ירוק וכחול כערכיו האפשריים. הרעיון הוא לקבל צבע כתכונה של אובייקטים אחרים שאתה יוצר, כגון אובייקטים לרכב:

מכונית מחלקה {צבע צבע; ...}

אז אתה יכול לכתוב קוד ברור וקריא, כך:

 myCar.color = אדום; 

במקום משהו כמו:

 myCar.color = 3; 

תכונה חשובה עוד יותר של קבועים המנויים בשפות כמו פסקל היא שהם סוגים בטוחים. במילים אחרות, לא ניתן להקצות צבע לא חוקי למאפיין הצבע - עליו להיות תמיד אדום, ירוק או כחול. לעומת זאת, אם משתנה הצבע היה אינט, אז תוכל להקצות לו כל מספר שלם חוקי, גם אם המספר הזה לא מייצג צבע חוקי.

מאמר זה נותן לך תבנית ליצירת קבועים קבועים שהם:

  • הקלד כספת
  • להדפסה
  • הוזמן, לשימוש כאינדקס
  • מקושר, לולאה קדימה או אחורה
  • ניתן למנות

במאמר עתידי תלמד כיצד להרחיב קבועים ספורים ליישום התנהגות תלוית מדינה.

מדוע לא להשתמש בגמר סטטי?

מנגנון נפוץ לקבועים קבועים משתמש במשתני int אחרונים סטטיים, כמו זה:

סופי סטטי int RED = 0; סופי סטטי int GREEN = 1; סופי סטטי int כחול = 2; ...

גמר סטטי שימושי

מכיוון שהם סופיים, הערכים קבועים ובלתי ניתנים לשינוי. מכיוון שהם סטטיים, הם נוצרים רק פעם אחת עבור המחלקה או הממשק שבו הם מוגדרים, במקום פעם אחת לכל אובייקט. ומכיוון שהם משתנים שלמים, ניתן למנות אותם ולהשתמש בהם כמדד.

לדוגמה, אתה יכול לכתוב לולאה כדי ליצור רשימה של הצבעים המועדפים על הלקוח:

עבור (int i = 0; ...) {if (customerLikesColor (i)) {favoriteColors.add (i); }}

ניתן גם להוסיף לאינדקס למערך או לווקטור באמצעות המשתנים כדי לקבל ערך המשויך לצבע. לדוגמא, נניח שיש לך משחק לוח שיש בו חלקים בצבעים שונים לכל שחקן. נניח שיש לך מפת סיביות לכל פיסת צבע ושיטה הנקראת display()להעתיק את מפת הסיביות למיקום הנוכחי. אחת הדרכים לשים חתיכה על הלוח עשויה להיות משהו כזה:

PiecePicture redPiece = PiecePicture חדש (RED); PiecePicture greenPiece = PiecePicture חדש (ירוק); PiecePicture bluePiece = PiecePicture חדש (כחול);

void placePiece (int מיקום, int צבע) {setPosition (מיקום); אם (צבע == אדום) {תצוגה (redPiece); } אחר אם (צבע == ירוק) {תצוגה (greenPiece); } אחר {תצוגה (bluePiece); }}

אך באמצעות ערכים שלמים לאינדקס למערך של חלקים, אתה יכול לפשט את הקוד ל:

PiecePicture [] piece = {PiecePicture new (RED), PiecePicture new (GREEN), PiecePicture new (BLUE)}; void placePiece (int מיקום, int צבע) {setPosition (מיקום); תצוגה (חתיכה [צבע]); }

היכולת לעבור על מגוון קבועים ולהוסיף לאינדקס למערך או וקטור הם היתרונות העיקריים של מספרים שלמים סופיים סטטיים. וכשמספר האפשרויות גדל, אפקט הפשט גדול עוד יותר.

אבל גמר סטטי מסוכן

ובכל זאת, ישנם כמה חסרונות בשימוש במספרים שלמים סופיים סטטיים. החיסרון העיקרי הוא היעדר בטיחות סוג. כל מספר שלם שמחושב או נקרא יכול לשמש כ"צבע ", לא משנה אם זה הגיוני לעשות זאת. אתה יכול לולאה ממש בסוף הקבועים המוגדרים או להפסיק לכסות את כולם, דבר שיכול בקלות לקרות אם אתה מוסיף או מסיר קבוע מהרשימה אך שוכח להתאים את אינדקס הלולאה.

לדוגמה, לולאת העדפת הצבעים שלך עשויה לקרוא כך:

עבור (int i = 0; i <= BLUE; i ++) {if (customerLikesColor (i)) {favoriteColors.add (i); }}

בהמשך, תוכל להוסיף צבע חדש:

סופי סטטי int RED = 0; סופי סטטי int GREEN = 1; סופי סטטי int כחול = 2; סופי סטטי int MAGENTA = 3;

לחלופין, תוכל להסיר אחד:

סופי סטטי int RED = 0; intical final int BLUE = 1;

בשני המקרים, התוכנית לא תפעל כראוי. אם תסיר צבע, תקבל שגיאת זמן ריצה שמפנה את תשומת הלב לבעיה. אם תוסיף צבע, לא תקבל שום שגיאה כלל - התוכנית פשוט לא תצליח לכסות את כל אפשרויות הצבע.

חסרון נוסף הוא היעדר מזהה קריא. אם אתה משתמש בתיבת הודעות או בפלט קונסולה כדי להציג את בחירת הצבע הנוכחית, תקבל מספר. זה הופך את הדיבורים לקשים למדי.

הבעיות ביצירת מזהה קריא נפתרות לפעמים באמצעות קבועי מחרוזת סופיים סטטיים, כך:

סופי סטטי מחרוזת RED = "red" .intern (); ...

Using the intern() method guarantees there is only one string with those contents in the internal string pool. But for intern() to be effective, every string or string variable that is ever compared to RED must use it. Even then, static final strings do not allow for looping or for indexing into an array, and they still do not address the issue of type safety.

Type safety

The problem with static final integers is that the variables that use them are inherently unbounded. They are int variables, which means they can hold any integer, not just the constants they were intended to hold. The goal is to define a variable of type Color so that you get a compilation error rather than a runtime error whenever an invalid value is assigned to that variable.

An elegant solution was provided in Philip Bishop's article in JavaWorld, "Typesafe constants in C++ and Java."

The idea is really simple (once you see it!):

public final class Color { // final class!! private Color() {} // private constructor!!

public static final Color RED = new Color(); public static final Color GREEN = new Color(); public static final Color BLUE = new Color(); }

Because the class is defined as final, it can't be subclassed. No other classes will be created from it. Because the constructor is private, other methods can't use the class to create new objects. The only objects that will ever be created with this class are the static objects the class creates for itself the first time the class is referenced! This implementation is a variation of the Singleton pattern that limits the class to a predefined number of instances. You can use this pattern to create exactly one class any time you need a Singleton, or use it as shown here to create a fixed number of instances. (The Singleton pattern is defined in the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, and Vlissides, Addison-Wesley, 1995. See the Resources section for a link to this book.)

The mind-boggling part of this class definition is that the class uses itself to create new objects. The first time you reference RED, it doesn't exist. But the act of accessing the class that RED is defined in causes it to be created, along with the other constants. Admittedly, that kind of recursive reference is rather difficult to visualize. But the advantage is total type safety. A variable of type Color can never be assigned anything other than the RED, GREEN, or BLUE objects that the Color class creates.

Identifiers

The first enhancement to the typesafe enumerated constant class is to create a string representation of the constants. You want to be able to produce a readable version of the value with a line like this:

 System.out.println(myColor); 

Whenever you output an object to a character output stream like System.out, and whenever you concatenate an object to a string, Java automatically invokes the toString() method for that object. That's a good reason to define a toString() method for any new class you create.

If the class does not have a toString() method, the inheritance hierarchy is inspected until one is found. At the top of the hierarchy, the toString() method in the Object class returns the class name. So the toString() method always has some meaning, but most of the time the default method will not be very useful.

Here is a modification to the Color class that provides a useful toString() method:

public final class Color { private String id; private Color(String anID) {this.id = anID; } public String toString() {return this.id; }

public static final Color RED = new Color(

"Red"

); public static final Color GREEN = new Color(

"Green"

); public static final Color BLUE = new Color(

"Blue"

); }

This version adds a private String variable (id). The constructor has been modified to take a String argument and store it as the object's ID. The toString() method then returns the object's ID.

One trick you can use to invoke the toString() method takes advantage of the fact that it is automatically invoked when an object is concatenated to a string. That means you could put the object's name in a dialog by concatenating it to a null string using a line like the following:

 textField1.setText("" + myColor); 

Unless you happen to love all the parentheses in Lisp, you will find that a bit more readable than the alternative:

 textField1.setText(myColor.toString()); 

It's also easier to make sure you put in the right number of closing parentheses!

Ordering and indexing

The next question is how to index into a vector or an array using members of the

Color

class. The mechanism will be to assign an ordinal number to each class constant and reference it using the attribute

.ord

, like this:

 void placePiece(int location, int color) { setPosition(location); display(piece[color.ord]); } 

Although tacking on .ord to convert the reference to color into a number is not particularly pretty, it is not terribly obtrusive either. It seems like a fairly reasonable tradeoff for typesafe constants.

Here is how the ordinal numbers are assigned:

public final class Color { private String id; public final int ord;private static int upperBound = 0; private Color(String anID) { this.id = anID; this.ord = upperBound++; } public String toString() {return this.id; } public static int size() { return upperBound; }

public static final Color RED = new Color("Red"); public static final Color GREEN = new Color("Green"); public static final Color BLUE = new Color("Blue"); }

This code uses the new JDK version 1.1 definition of a "blank final" variable -- a variable that is assigned a value once and once only. This mechanism allows each object to have its own non-static final variable, ord, which will be assigned once during object creation and which will thereafter remain immutable. The static variable upperBound keeps track of the next unused index in the collection. That value becomes the ord attribute when the object is created, after which the upper bound is incremented.

For compatibility with the Vector class, the method size() is defined to return the number of constants that have been defined in this class (which is the same as the upper bound).

A purist might decide that the variable ord should be private, and the method named ord() should return it -- if not, a method named getOrd(). I lean toward accessing the attribute directly, though, for two reasons. The first is that the concept of an ordinal is unequivocally that of an int. There is little likelihood, if any, that the implementation would ever change. The second reason is that what you really want is the ability to use the object as though it were an int, as you could in a language like Pascal. For example, you might want to use the attribute color to index an array. But you cannot use a Java object to do that directly. What you would really like to say is:

 display(piece[color]); // desirable, but does not work 

But you can't do that. The minimum change necessary to get what you want is to access an attribute, instead, like this:

 display(piece[color.ord]); // closest to desirable 

instead of the lengthy alternative:

 display(piece[color.ord()]); // extra parentheses 

or the even lengthier:

 display(piece[color.getOrd()]); // extra parentheses and text 

The Eiffel language uses the same syntax for accessing attributes and invoking methods. That would be the ideal. Given the necessity of choosing one or the other, however, I've gone with accessing ord as an attribute. With any luck, the identifier ord will become so familiar as a result of repetition that using it will seem as natural as writing int. (As natural as that may be.)

Looping

השלב הבא הוא היכולת לחזור על קבועי הכיתה. אתה רוצה להיות מסוגל לולאה מההתחלה ועד הסוף:

 עבור (צבע c = Color.first (); c! = null; c = c.next ()) {...} 

או מהסוף חזרה להתחלה:

 עבור (צבע c = Color.last (); c! = null; c = c.prev ()) {...} 

שינויים אלה משתמשים במשתנים סטטיים כדי לעקוב אחר האובייקט האחרון שנוצר ולקשר אותו לאובייקט הבא: