ใช้ประเภทคงที่เพื่อรหัสที่ปลอดภัยและสะอาดยิ่งขึ้น

ในบทช่วยสอนนี้จะขยายแนวคิดเรื่องค่าคงที่แจกแจงตามที่กล่าวไว้ใน Eric Armstrong ของ "สร้างค่าคงที่แจกแจงใน Java" ฉันขอแนะนำอย่างยิ่งให้อ่านบทความนั้นก่อนที่คุณจะดื่มด่ำกับบทความนี้เนื่องจากฉันจะถือว่าคุณคุ้นเคยกับแนวคิดที่เกี่ยวข้องกับค่าคงที่ที่แจกแจงและฉันจะขยายความเกี่ยวกับโค้ดตัวอย่างบางส่วนที่ Eric นำเสนอ

แนวคิดของค่าคงที่

ในการจัดการกับค่าคงที่แจกแจงฉันจะพูดถึงส่วนที่แจกแจงของแนวคิดในตอนท้ายของบทความ สำหรับตอนนี้เราจะเน้นเฉพาะด้านคงที่ค่าคงที่เป็นตัวแปรที่ค่าไม่สามารถเปลี่ยนแปลงได้ ใน C / C ++ คีย์เวิร์ดconstจะใช้เพื่อประกาศตัวแปรคงที่เหล่านี้ ใน Java finalคุณใช้คำหลักอย่างไรก็ตามเครื่องมือที่นำมาใช้ในที่นี้ไม่ใช่แค่ตัวแปรดั้งเดิม มันเป็นอินสแตนซ์ของวัตถุจริง อินสแตนซ์ออบเจ็กต์ไม่เปลี่ยนรูปและไม่สามารถเปลี่ยนแปลงได้ - สถานะภายในไม่สามารถแก้ไขได้ สิ่งนี้คล้ายกับรูปแบบซิงเกิลตันที่คลาสสามารถมีได้เพียงอินสแตนซ์เดียว อย่างไรก็ตามในกรณีนี้คลาสอาจมีชุดอินสแตนซ์ที่ จำกัด และกำหนดไว้ล่วงหน้าเท่านั้น

เหตุผลหลักในการใช้ค่าคงที่คือความชัดเจนและความปลอดภัย ตัวอย่างเช่นโค้ดต่อไปนี้ไม่สามารถอธิบายตนเองได้:

โมฆะสาธารณะ setColor (int x) {... } โมฆะสาธารณะ someMethod () {setColor (5); }

จากรหัสนี้เราสามารถตรวจสอบได้ว่ากำลังกำหนดสี แต่สี 5 หมายถึงอะไร? หากโค้ดนี้เขียนโดยโปรแกรมเมอร์ที่หายากซึ่งแสดงความคิดเห็นเกี่ยวกับงานของเขาเราอาจพบคำตอบที่ด้านบนของไฟล์ แต่มีโอกาสมากกว่าที่เราจะต้องค้นหาเอกสารการออกแบบเก่า ๆ (ถ้ามี) เพื่อหาคำอธิบาย

วิธีแก้ปัญหาที่ชัดเจนยิ่งขึ้นคือการกำหนดค่า 5 ให้กับตัวแปรที่มีชื่อที่มีความหมาย ตัวอย่างเช่น:

สาธารณะคง int สุดท้ายสีแดง = 5; โมฆะสาธารณะ someMethod () {setColor (RED); }

ตอนนี้เราสามารถบอกได้ทันทีว่าเกิดอะไรขึ้นกับรหัส สีจะถูกตั้งค่าเป็นสีแดง แบบนี้สะอาดกว่า แต่ปลอดภัยกว่าไหม? จะเกิดอะไรขึ้นถ้า coder อื่นสับสนและประกาศค่าที่แตกต่างกันดังนี้:

สาธารณะคง int สุดท้ายสีแดง = 3; สาธารณะคง int สุดท้ายสีเขียว = 5;

ตอนนี้เรามีปัญหาสองอย่าง ก่อนอื่นREDไม่ได้ตั้งค่าเป็นค่าที่ถูกต้องอีกต่อไป GREENประการที่สองค่าสำหรับสีแดงเป็นตัวแทนจากตัวแปรที่มีชื่อ บางทีส่วนที่น่ากลัวที่สุดคือโค้ดนี้จะคอมไพล์ได้ดีและอาจตรวจไม่พบจุดบกพร่องจนกว่าสินค้าจะถูกจัดส่ง

เราสามารถแก้ไขปัญหานี้ได้โดยการสร้างคลาสสีขั้นสุดท้าย:

สีระดับสาธารณะ {public static final int RED = 5; สาธารณะคง int สุดท้ายสีเขียว = 7; }

จากนั้นผ่านการตรวจสอบเอกสารและโค้ดเราขอแนะนำให้โปรแกรมเมอร์ใช้มันดังนี้:

โมฆะสาธารณะ someMethod () {setColor (Color.RED); }

ฉันพูดให้กำลังใจเพราะการออกแบบในรายการรหัสนั้นไม่อนุญาตให้เราบังคับให้ผู้เขียนโค้ดปฏิบัติตาม โค้ดจะยังคงคอมไพล์แม้ว่าทุกอย่างจะไม่เป็นไปตามลำดับก็ตาม ดังนั้นแม้ว่าจะปลอดภัยกว่าเล็กน้อย แต่ก็ไม่ปลอดภัยอย่างสมบูรณ์ แม้ว่าโปรแกรมเมอร์ควรใช้Colorคลาส แต่ก็ไม่จำเป็นต้องทำ โปรแกรมเมอร์สามารถเขียนและรวบรวมโค้ดต่อไปนี้ได้อย่างง่ายดาย:

 setColor (3498910); 

ที่ไม่setColorวิธีการรับรู้จำนวนมากนี้จะเป็นสีหรือไม่? อาจจะไม่. แล้วเราจะป้องกันตัวเองจากโปรแกรมเมอร์จอมโกงเหล่านี้ได้อย่างไร? นั่นคือจุดที่ประเภทค่าคงที่เข้ามาช่วย

เราเริ่มต้นด้วยการกำหนดลายเซ็นของวิธีการใหม่:

 โมฆะสาธารณะ setColor (สี x) {... } 

ตอนนี้โปรแกรมเมอร์ไม่สามารถส่งผ่านค่าจำนวนเต็มโดยพลการ พวกเขาถูกบังคับให้จัดหาColorวัตถุที่ถูกต้อง ตัวอย่างการใช้งานอาจมีลักษณะดังนี้:

โมฆะสาธารณะ someMethod () {setColor (สีใหม่ ("Red")); }

เรายังคงทำงานกับรหัสที่สะอาดและอ่านได้และเราใกล้จะบรรลุความปลอดภัยอย่างแท้จริงมากขึ้น แต่เรายังไม่ค่อยมี โปรแกรมเมอร์ยังคงมีที่ว่างให้สร้างความหายนะและสามารถสร้างสีใหม่ ๆ ได้ตามอำเภอใจดังนี้:

โมฆะสาธารณะ someMethod () {setColor (สีใหม่ ("สวัสดีฉันชื่อ Ted.")); }

เราป้องกันสถานการณ์นี้โดยทำให้Colorคลาสไม่เปลี่ยนรูปและซ่อนอินสแตนซ์จากโปรแกรมเมอร์ เราสร้างสีแต่ละประเภท (แดงเขียวน้ำเงิน) ให้เป็นซิงเกิลตัน สิ่งนี้ทำได้โดยทำให้ตัวสร้างเป็นแบบส่วนตัวจากนั้นเปิดเผยแฮนเดิลสาธารณะไปยังรายการอินสแตนซ์ที่ จำกัด และกำหนดไว้อย่างดี:

ระดับสาธารณะสี {สีส่วนตัว () {} สีสุดท้ายคงที่สาธารณะสีแดง = สีใหม่ (); สาธารณะคงสุดท้ายสีเขียว = สีใหม่ (); สาธารณะคงสุดท้ายสีฟ้า = สีใหม่ (); }

ในรหัสนี้เราได้รับความปลอดภัยอย่างแท้จริงในที่สุด โปรแกรมเมอร์ไม่สามารถสร้างสีปลอมได้ สามารถใช้ได้เฉพาะสีที่กำหนดไว้เท่านั้น มิฉะนั้นโปรแกรมจะไม่คอมไพล์ นี่คือลักษณะการใช้งานของเราตอนนี้:

โมฆะสาธารณะ someMethod () {setColor (Color.RED); }

วิริยะ

เอาล่ะตอนนี้เรามีวิธีจัดการกับประเภทคงที่ที่สะอาดและปลอดภัยแล้ว เราสามารถสร้างวัตถุที่มีแอตทริบิวต์สีและมั่นใจได้ว่าค่าสีจะถูกต้องเสมอ แต่ถ้าเราต้องการเก็บวัตถุนี้ไว้ในฐานข้อมูลหรือเขียนลงในไฟล์ล่ะ? เราจะบันทึกค่าสีได้อย่างไร? เราต้องแมปประเภทเหล่านี้กับค่า

ในบทความJavaWorld ที่กล่าวถึงข้างต้น Eric Armstrong ใช้ค่าสตริง การใช้สตริงช่วยเพิ่มโบนัสในการให้สิ่งที่มีความหมายแก่คุณในการส่งคืนในtoString()เมธอดซึ่งทำให้เอาต์พุตการดีบักชัดเจนมาก

แม้ว่าสตริงอาจมีราคาแพงในการจัดเก็บ จำนวนเต็มต้องการ 32 บิตในการจัดเก็บค่าในขณะที่สตริงต้องการ 16 บิตต่ออักขระ (เนื่องจากการรองรับ Unicode) ตัวอย่างเช่นหมายเลข 49858712 สามารถจัดเก็บได้ใน 32 บิต แต่สตริงTURQUOISEจะต้องใช้ 144 บิต หากคุณกำลังจัดเก็บวัตถุหลายพันรายการที่มีแอตทริบิวต์สีความแตกต่างเล็กน้อยในบิต (ระหว่าง 32 ถึง 144 ในกรณีนี้) สามารถรวมกันได้อย่างรวดเร็ว ลองใช้ค่าจำนวนเต็มแทน วิธีแก้ปัญหานี้คืออะไร? เราจะคงค่าสตริงไว้เนื่องจากมีความสำคัญต่อการนำเสนอ แต่เราจะไม่เก็บค่าเหล่านี้

เวอร์ชันของ Java ตั้งแต่ 1.1 บนสามารถทำให้เป็นซีเรียลไลซ์อ็อบเจ็กต์โดยอัตโนมัติตราบเท่าที่ใช้Serializableอินเทอร์เฟซ เพื่อป้องกันไม่ให้ Java จัดเก็บข้อมูลภายนอกคุณต้องประกาศตัวแปรดังกล่าวด้วยtransientคีย์เวิร์ด ดังนั้นในการจัดเก็บค่าจำนวนเต็มโดยไม่เก็บการแสดงสตริงเราจึงประกาศแอตทริบิวต์สตริงเป็นแบบชั่วคราว นี่คือคลาสใหม่พร้อมด้วยตัวเข้าถึงคุณลักษณะจำนวนเต็มและสตริง:

คลาสสาธารณะสีใช้ java.io.Serializable {private int value; ชื่อสตริงชั่วคราวส่วนตัว สุดท้ายคงที่สาธารณะสีแดง = สีใหม่ (0, "แดง"); สาธารณะคงสุดท้ายสีฟ้า = สีใหม่ (1, "สีน้ำเงิน"); สาธารณะคงสุดท้ายสีเขียว = สีใหม่ (2, "สีเขียว"); สีส่วนตัว (ค่า int, ชื่อสตริง) {this.value = value; this.name = ชื่อ; } int สาธารณะ getValue () {return value; } สตริงสาธารณะ toString () {return name; }}

Colorตอนนี้เราได้อย่างมีประสิทธิภาพสามารถเก็บตัวอย่างของประเภทอย่างต่อเนื่อง แต่สิ่งที่เกี่ยวกับการฟื้นฟูพวกเขา? นั่นจะเป็นเรื่องยุ่งยากเล็กน้อย ก่อนที่เราจะดำเนินการต่อไปเรามาขยายความเป็นกรอบที่จะจัดการกับข้อผิดพลาดทั้งหมดที่กล่าวมาข้างต้นเพื่อให้เราสามารถมุ่งเน้นไปที่เรื่องง่ายๆของการกำหนดประเภทได้

กรอบประเภทค่าคงที่

With our firm understanding of constant types, I can now jump into this month's tool. The tool is called Type and it is a simple abstract class. All you have to do is create a very simple subclass and you've got a full-featured constant type library. Here's what our Color class will look like now:

public class Color extends Type { protected Color( int value, String desc ) { super( value, desc ); } public static final Color RED = new Color( 0, "Red" ); public static final Color BLUE = new Color( 1, "Blue" ); public static final Color GREEN = new Color( 2, "Green" ); } 

The Color class consists of nothing but a constructor and a few publicly accessible instances. All of the logic discussed to this point will be defined and implemented in the superclass Type; we'll be adding more as we go along. Here's what Type looks like so far:

public class Type implements java.io.Serializable { private int value; private transient String name; protected Type( int value, String name ) { this.value = value; this.name = name; } public int getValue() { return value; } public String toString() { return name; } } 

Back to persistence

With our new framework in hand, we can continue where we left off in the discussion of persistence. Remember, we can save our types by storing their integer values, but now we want to restore them. This is going to require a lookup -- a reverse calculation to locate the object instance based on its value. In order to perform a lookup, we need a way to enumerate all of the possible types.

In Eric's article, he implemented his own enumeration by implementing the constants as nodes in a linked list. I'm going to forego this complexity and use a simple hashtable instead. The key for the hash will be the integer values of the type (wrapped in an Integer object), and the value of the hash will be a reference to the type instance. For example, the GREEN instance of Color would be stored like so:

 hashtable.put( new Integer( GREEN.getValue() ), GREEN ); 

Of course, we don't want to type this out for each possible type. There could be hundreds of different values, thus creating a typing nightmare and opening the doors to some nasty problems -- you might forget to put one of the values in the hashtable and then not be able to look it up later, for instance. So we'll declare a global hashtable within Type and modify the constructor to store the mapping upon creation:

 private static final Hashtable types = new Hashtable(); protected Type( int value, String desc ) { this.value = value; this.desc = desc; types.put( new Integer( value ), this ); } 

But this creates a problem. If we have a subclass called Color, which has a type (that is, Green) with a value of 5, and then we create another subclass called Shade, which also has a type (that is Dark) with a value of 5, only one of them will be stored in the hashtable -- the last one to be instantiated.

In order to avoid this, we have to store a handle to the type based on not only its value, but also its class. Let's create a new method to store the type references. We'll use a hashtable of hashtables. The inner hashtable will be a mapping of values to types for each specific subclass (Color, Shade, and so on). The outer hashtable will be a mapping of subclasses to inner tables.

This routine will first attempt to acquire the inner table from the outer table. If it receives a null, the inner table doesn't exist yet. So, we create a new inner table and put it into the outer table. Next, we add the value/type mapping to the inner table and we're done. Here's the code:

 private void storeType( Type type ) { String className = type.getClass().getName(); Hashtable values; synchronized( types ) // avoid race condition for creating inner table { values = (Hashtable) types.get( className ); if( values == null ) { values = new Hashtable(); types.put( className, values ); } } values.put( new Integer( type.getValue() ), type ); } 

And here's the new version of the constructor:

 protected Type( int value, String desc ) { this.value = value; this.desc = desc; storeType( this ); } 

Now that we are storing a road map of types and values, we can perform lookups and thus restore an instance based on a value. The lookup requires two things: the target subclass identity and the integer value. Using this information, we can extract the inner table and find the handle to the matching type instance. Here's the code:

 public static Type getByValue( Class classRef, int value ) { Type type = null; String className = classRef.getName(); Hashtable values = (Hashtable) types.get( className ); if( values != null ) { type = (Type) values.get( new Integer( value ) ); } return( type ); } 

Thus, restoring a value is as simple as this (note that the return value must be casted):

 int value = // read from file, database, etc. Color background = (ColorType) Type.findByValue( ColorType.class, value ); 

Enumerating the types

ต้องขอบคุณองค์กรแฮชแท็กของแฮชแท็กของเรามันง่ายมากที่จะเปิดเผยฟังก์ชันการแจงนับที่นำเสนอโดยการใช้งานของ Eric ข้อแม้ประการเดียวคือไม่รับประกันการจัดเรียงตามการออกแบบของ Eric หากคุณใช้ Java 2 คุณสามารถแทนที่แผนที่ที่เรียงลำดับสำหรับแฮชแท็กภายในได้ แต่อย่างที่ฉันได้ระบุไว้ในตอนต้นของคอลัมน์นี้ตอนนี้ฉันกังวลกับ JDK เวอร์ชัน 1.1 เท่านั้น

ตรรกะเดียวที่จำเป็นในการระบุประเภทคือการดึงข้อมูลตารางด้านในและส่งคืนรายการองค์ประกอบ หากไม่มีตารางด้านในเราก็ส่งคืนค่าว่าง นี่คือวิธีการทั้งหมด: