เราจะทำให้คลาส Java ไม่เปลี่ยนรูปได้อย่างไรความต้องการความไม่เปลี่ยนรูปคืออะไรและมีข้อได้เปรียบในการใช้สิ่งนี้หรือไม่?
@Immutable
คำอธิบายประกอบนี้จากjcabi ด้าน
เราจะทำให้คลาส Java ไม่เปลี่ยนรูปได้อย่างไรความต้องการความไม่เปลี่ยนรูปคืออะไรและมีข้อได้เปรียบในการใช้สิ่งนี้หรือไม่?
@Immutable
คำอธิบายประกอบนี้จากjcabi ด้าน
คำตอบ:
วัตถุที่ไม่เปลี่ยนรูปคืออะไร?
วัตถุที่ไม่เปลี่ยนรูปคือวัตถุที่จะไม่เปลี่ยนสถานะหลังจากที่สร้างอินสแตนซ์แล้ว
จะทำให้วัตถุไม่เปลี่ยนรูปได้อย่างไร?
โดยทั่วไปออบเจ็กต์ที่ไม่เปลี่ยนรูปสามารถทำได้โดยการกำหนดคลาสที่ไม่มีสมาชิกใด ๆ เปิดเผยและไม่มีตัวเซ็ต
คลาสต่อไปนี้จะสร้างวัตถุที่ไม่เปลี่ยนรูป:
class ImmutableInt {
private final int value;
public ImmutableInt(int i) {
value = i;
}
public int getValue() {
return value;
}
}
ดังที่เห็นในตัวอย่างข้างต้นค่าของImmutableInt
สามารถตั้งค่าได้ก็ต่อเมื่อวัตถุถูกสร้างอินสแตนซ์และโดยมีเพียง getter (getValue
) สถานะของอ็อบเจ็กต์จะไม่สามารถเปลี่ยนแปลงได้หลังจากสร้างอินสแตนซ์
อย่างไรก็ตามต้องใช้ความระมัดระวังว่าวัตถุทั้งหมดที่อ้างอิงโดยวัตถุนั้นจะต้องไม่เปลี่ยนรูปเช่นกันหรืออาจเป็นไปได้ที่จะเปลี่ยนสถานะของวัตถุ
ตัวอย่างเช่นการอนุญาตให้อ้างอิงไปยังอาร์เรย์หรือArrayList
ได้รับผ่าน getter จะทำให้สถานะภายในเปลี่ยนแปลงได้โดยการเปลี่ยนอาร์เรย์หรือคอลเลกชัน:
class NotQuiteImmutableList<T> {
private final List<T> list;
public NotQuiteImmutableList(List<T> list) {
// creates a new ArrayList and keeps a reference to it.
this.list = new ArrayList(list);
}
public List<T> getList() {
return list;
}
}
ปัญหาเกี่ยวกับรหัสข้างต้นคือArrayList
สามารถรับผ่านgetList
และถูกจัดการได้ซึ่งนำไปสู่สถานะของวัตถุที่จะเปลี่ยนแปลงดังนั้นจึงไม่เปลี่ยนรูป
// notQuiteImmutableList contains "a", "b", "c"
List<String> notQuiteImmutableList= new NotQuiteImmutableList(Arrays.asList("a", "b", "c"));
// now the list contains "a", "b", "c", "d" -- this list is mutable.
notQuiteImmutableList.getList().add("d");
วิธีหนึ่งในการแก้ไขปัญหานี้คือส่งคืนสำเนาของอาร์เรย์หรือคอลเลกชันเมื่อเรียกจาก getter:
public List<T> getList() {
// return a copy of the list so the internal state cannot be altered
return new ArrayList(list);
}
ข้อดีของการไม่เปลี่ยนรูปคืออะไร?
ข้อได้เปรียบของการไม่เปลี่ยนรูปมาพร้อมกับการทำงานพร้อมกัน เป็นการยากที่จะรักษาความถูกต้องในอ็อบเจ็กต์ที่ไม่แน่นอนเนื่องจากเธรดหลายเธรดอาจพยายามเปลี่ยนสถานะของอ็อบเจ็กต์เดียวกันทำให้เธรดบางเธรดเห็นสถานะที่แตกต่างกันของอ็อบเจ็กต์เดียวกันขึ้นอยู่กับเวลาของการอ่านและเขียนไปยังสิ่งที่กล่าว วัตถุ.
ด้วยการมีวัตถุที่ไม่เปลี่ยนรูปเราสามารถมั่นใจได้ว่าเธรดทั้งหมดที่มองไปที่วัตถุจะเห็นสถานะเดียวกันเนื่องจากสถานะของวัตถุที่ไม่เปลี่ยนรูปจะไม่เปลี่ยนแปลง
return Collections.unmodifiableList(list);
ส่งคืนมุมมองแบบอ่านอย่างเดียวในรายการได้
final
ด้วย มิฉะนั้นสามารถขยายด้วยเมธอด setter (หรือวิธีการกลายพันธุ์ชนิดอื่น ๆ และฟิลด์ที่เปลี่ยนแปลงได้)
final
ถ้าคลาสมีเฉพาะprivate
เขตข้อมูลเนื่องจากไม่สามารถเข้าถึงได้จากคลาสย่อย
นอกเหนือจากคำตอบที่ให้ไปแล้วฉันขอแนะนำให้อ่านเกี่ยวกับความไม่เปลี่ยนรูปใน Effective Java, 2nd Ed. เนื่องจากมีรายละเอียดบางอย่างที่พลาดง่าย (เช่นสำเนาป้องกัน) นอกจากนี้ Java 2nd Ed ที่มีประสิทธิภาพ เป็นสิ่งที่ต้องอ่านสำหรับนักพัฒนา Java ทุกคน
คุณทำให้คลาสไม่เปลี่ยนรูปแบบนี้:
public final class Immutable
{
private final String name;
public Immutable(String name)
{
this.name = name;
}
public String getName() { return this.name; }
// No setter;
}
ต่อไปนี้เป็นข้อกำหนดในการทำให้คลาส Java ไม่เปลี่ยนรูป:
final
(เพื่อให้ไม่สามารถสร้างคลาสย่อยได้)final
(ดังนั้นเราจึงไม่สามารถเปลี่ยนค่าของมันได้หลังจากสร้างวัตถุ)คลาสที่ไม่เปลี่ยนรูปมีประโยชน์เนื่องจาก
- ปลอดภัยต่อเธรด
- พวกเขายังแสดงบางสิ่งบางอย่างที่ลึกซึ้งเกี่ยวกับการออกแบบของคุณ: "ไม่สามารถเปลี่ยนแปลงสิ่งนี้ได้" เมื่อนำไปใช้ก็เป็นสิ่งที่คุณต้องการ
ความไม่เปลี่ยนรูปสามารถทำได้โดยส่วนใหญ่สองวิธี:
final
แอตทริบิวต์อินสแตนซ์เพื่อหลีกเลี่ยงการมอบหมายใหม่ข้อดีของการไม่เปลี่ยนรูปคือสมมติฐานที่คุณสามารถทำได้กับวัตถุเหล่านี้:
คลาสที่ไม่เปลี่ยนรูปไม่สามารถกำหนดค่าใหม่ได้หลังจากสร้างอินสแตนซ์แล้วตัวสร้างกำหนดค่าให้กับตัวแปรส่วนตัว จนกว่าอ็อบเจ็กต์จะกลายเป็นโมฆะจะไม่สามารถเปลี่ยนค่าได้เนื่องจากเมธอด setter ไม่พร้อมใช้งาน
ไม่เปลี่ยนรูปควรตอบสนองต่อไปนี้
/**
* Strong immutability - by making class final
*/
public final class TestImmutablity {
// make the variables private
private String Name;
//assign value when the object created
public TestImmutablity(String name) {
this.Name = name;
}
//provide getters to access values
public String getName() {
return this.Name;
}
}
ข้อได้เปรียบ: วัตถุที่ไม่เปลี่ยนรูปมีค่าเริ่มต้นจนกว่ามันจะตาย
คลาสไม่เปลี่ยนรูปคือที่ไม่สามารถเปลี่ยนแปลงวัตถุได้หลังจากสร้าง
คลาสที่ไม่เปลี่ยนรูปมีประโยชน์สำหรับ
ตัวอย่าง
คลาสสตริง
ตัวอย่างรหัส
public final class Student {
private final String name;
private final String rollNumber;
public Student(String name, String rollNumber) {
this.name = name;
this.rollNumber = rollNumber;
}
public String getName() {
return this.name;
}
public String getRollNumber() {
return this.rollNumber;
}
}
เราจะทำให้คลาส Java ไม่เปลี่ยนรูปได้อย่างไร
จาก JDK 14+ ซึ่งมีJEP 359เราสามารถใช้ "records
" ได้ เป็นวิธีที่ง่ายที่สุดและไม่เร่งรีบในการสร้างคลาสที่ไม่เปลี่ยนรูปแบบ
คลาสเร็กคอร์ดเป็นพาหะที่โปร่งใสและไม่เปลี่ยนรูปตื้น ๆสำหรับชุดฟิลด์คงที่ซึ่งเรียกว่าเร็กคอร์ดcomponents
ที่ให้state
คำอธิบายสำหรับเร็กคอร์ด แต่ละฟิลด์component
ก่อให้เกิดfinal
ฟิลด์ที่เก็บค่าที่ระบุและaccessor
วิธีการดึงค่า ชื่อฟิลด์และชื่อผู้เข้าถึงตรงกับชื่อของคอมโพเนนต์
ลองพิจารณาตัวอย่างของการสร้างสี่เหลี่ยมผืนผ้าที่ไม่เปลี่ยนรูป
record Rectangle(double length, double width) {}
ไม่จำเป็นต้องประกาศตัวสร้างใด ๆ ไม่จำเป็นต้องใช้เมธอด equals & hashCode เพียงแค่บันทึกใด ๆ ต้องมีชื่อและคำอธิบายสถานะ
var rectangle = new Rectangle(7.1, 8.9);
System.out.print(rectangle.length()); // prints 7.1
หากคุณต้องการตรวจสอบความถูกต้องของค่าในระหว่างการสร้างวัตถุเราต้องประกาศตัวสร้างอย่างชัดเจน
public Rectangle {
if (length <= 0.0) {
throw new IllegalArgumentException();
}
}
เนื้อหาของเร็กคอร์ดอาจประกาศวิธีการแบบคงที่ฟิลด์แบบคงที่ตัวเริ่มต้นแบบคงที่ตัวสร้างวิธีการอินสแตนซ์และประเภทที่ซ้อนกัน
วิธีการอินสแตนซ์
record Rectangle(double length, double width) {
public double area() {
return this.length * this.width;
}
}
เขตข้อมูลคงวิธี
เนื่องจากสถานะควรเป็นส่วนหนึ่งของส่วนประกอบเราจึงไม่สามารถเพิ่มฟิลด์อินสแตนซ์ลงในเรกคอร์ดได้ แต่เราสามารถเพิ่มฟิลด์และวิธีการคงที่:
record Rectangle(double length, double width) {
static double aStaticField;
static void aStaticMethod() {
System.out.println("Hello Static");
}
}
อะไรคือความจำเป็นของการไม่เปลี่ยนรูปและมีประโยชน์ในการใช้สิ่งนี้หรือไม่?
คำตอบที่โพสต์ไว้ก่อนหน้านี้ดีพอที่จะพิสูจน์ความจำเป็นของการไม่เปลี่ยนรูปและเป็นข้อดี
อีกวิธีในการสร้างวัตถุที่ไม่เปลี่ยนรูปคือการใช้ไลบรารีImmutables.org :
สมมติว่ามีการเพิ่มการอ้างอิงที่จำเป็นสร้างคลาสนามธรรมด้วยวิธีการเข้าถึงนามธรรม คุณสามารถทำได้โดยการใส่คำอธิบายประกอบกับอินเทอร์เฟซหรือแม้แต่คำอธิบายประกอบ (@interface):
package info.sample;
import java.util.List;
import java.util.Set;
import org.immutables.value.Value;
@Value.Immutable
public abstract class FoobarValue {
public abstract int foo();
public abstract String bar();
public abstract List<Integer> buz();
public abstract Set<Long> crux();
}
ตอนนี้คุณสามารถสร้างและใช้การใช้งานที่ไม่เปลี่ยนรูปได้แล้ว:
package info.sample;
import java.util.List;
public class FoobarValueMain {
public static void main(String... args) {
FoobarValue value = ImmutableFoobarValue.builder()
.foo(2)
.bar("Bar")
.addBuz(1, 3, 4)
.build(); // FoobarValue{foo=2, bar=Bar, buz=[1, 3, 4], crux={}}
int foo = value.foo(); // 2
List<Integer> buz = value.buz(); // ImmutableList.of(1, 3, 4)
}
}
คลาสที่ไม่เปลี่ยนรูปเป็นเพียงคลาสที่ไม่สามารถแก้ไขอินสแตนซ์ได้
ข้อมูลทั้งหมดที่มีอยู่ในแต่ละอินสแตนซ์ได้รับการแก้ไขสำหรับอายุการใช้งานของวัตถุดังนั้นจึงไม่สามารถสังเกตการเปลี่ยนแปลงได้
คลาสที่ไม่เปลี่ยนรูปจะออกแบบนำไปใช้และใช้งานได้ง่ายกว่าคลาสที่เปลี่ยนแปลงไม่ได้
ในการทำให้คลาสไม่เปลี่ยนรูปให้ปฏิบัติตามกฎห้าข้อต่อไปนี้:
อย่าระบุวิธีการที่ปรับเปลี่ยนสถานะของวัตถุ
ตรวจสอบให้แน่ใจว่าไม่สามารถขยายชั้นเรียนได้
ทำให้ฟิลด์ทั้งหมดเป็นที่สิ้นสุด
ทำให้ฟิลด์ทั้งหมดเป็นแบบส่วนตัว
ตรวจสอบให้แน่ใจว่าสามารถเข้าถึงส่วนประกอบที่ไม่แน่นอนได้
วัตถุที่ไม่เปลี่ยนรูปนั้นปลอดภัยต่อเกลียวโดยเนื้อแท้ พวกเขาไม่ต้องการการซิงโครไนซ์
สามารถแบ่งปันวัตถุที่ไม่เปลี่ยนรูปได้อย่างอิสระ
วัตถุที่ไม่เปลี่ยนรูปเป็นส่วนประกอบที่ยอดเยี่ยมสำหรับวัตถุอื่น ๆ
@Jack การมีฟิลด์สุดท้ายและตัวเซ็ตในคลาสจะไม่ทำให้คลาสไม่เปลี่ยนรูป คำหลักสุดท้ายตรวจสอบให้แน่ใจว่าไม่มีการกำหนดตัวแปรใหม่ คุณต้องส่งคืนสำเนาลึกของฟิลด์ทั้งหมดในวิธีการ getter สิ่งนี้จะทำให้แน่ใจว่าหลังจากได้รับวัตถุจากเมธอด getter แล้วสถานะภายในของวัตถุจะไม่ถูกรบกวน
ในฐานะที่ไม่ใช่เจ้าของภาษาอังกฤษฉันไม่ชอบการตีความทั่วไปของ "คลาสที่ไม่เปลี่ยนรูป" ว่า "อ็อบเจ็กต์คลาสที่สร้างขึ้นไม่เปลี่ยนรูป"; แต่ฉันเองก็เอนเอียงที่จะตีความว่าเป็น "วัตถุคลาสนั้นไม่เปลี่ยนรูป"
ที่กล่าวว่า "คลาสไม่เปลี่ยนรูป" เป็นวัตถุที่ไม่เปลี่ยนรูป ความแตกต่างคือเมื่อตอบว่าประโยชน์คืออะไร สำหรับความรู้ / การตีความของฉันคลาสที่ไม่เปลี่ยนรูปจะป้องกันไม่ให้อ็อบเจ็กต์ถูกปรับเปลี่ยนพฤติกรรมรันไทม์
คำตอบส่วนใหญ่ที่นี่เป็นคำตอบที่ดีและบางคนก็พูดถึงกฎ แต่ฉันรู้สึกดีที่จะพูดถึงเหตุผลและเวลาที่เราต้องปฏิบัติตามกฎเหล่านี้ ดังนั้นฉันจึงให้คำอธิบายด้านล่าง
และแน่นอนว่าถ้าเราพยายามใช้ Setters สำหรับตัวแปรสุดท้ายคอมไพเลอร์จะแสดงข้อผิดพลาด
public class ImmutableClassExplored {
public final int a;
public final int b;
/* OR
Generally we declare all properties as private, but declaring them as public
will not cause any issues in our scenario if we make them final
public final int a = 109;
public final int b = 189;
*/
ImmutableClassExplored(){
this. a = 111;
this.b = 222;
}
ImmutableClassExplored(int a, int b){
this.a = a;
this.b= b;
}
}
เราจำเป็นต้องประกาศชั้นเรียนเป็น 'ขั้นสุดท้าย' หรือไม่?
1. มีเฉพาะสมาชิกดั้งเดิม:เราไม่มีปัญหาหากคลาสมีสมาชิกดั้งเดิมเท่านั้นเราไม่จำเป็นต้องประกาศคลาสเป็นขั้นสุดท้าย
2. การมีออบเจ็กต์เป็นตัวแปรสมาชิก:หากเรามีอ็อบเจกต์เป็นตัวแปรสมาชิกเราจะต้องทำให้สมาชิกของอ็อบเจ็กต์เหล่านั้นสิ้นสุดลงด้วย หมายความว่าเราต้องสำรวจลึกลงไปตามต้นไม้และทำให้วัตถุ / วัตถุดั้งเดิมทั้งหมดเป็นขั้นสุดท้ายซึ่งอาจเป็นไปไม่ได้ตลอดเวลา ดังนั้นวิธีแก้ปัญหาคือทำให้คลาสสุดท้ายซึ่งป้องกันการสืบทอด ดังนั้นจึงไม่มีคำถามเกี่ยวกับคลาสย่อยที่ลบล้างเมธอด getter
คำอธิบายประกอบ @ มูลค่าของลอมบอกสามารถใช้เพื่อสร้างคลาสที่ไม่เปลี่ยนรูปได้ ง่ายๆเหมือนโค้ดด้านล่าง
@Value
public class LombokImmutable {
int id;
String name;
}
ตามเอกสารในเว็บไซต์ของลอมบอก:
@Value เป็นตัวแปรที่ไม่เปลี่ยนรูปของ @Data; ฟิลด์ทั้งหมดถูกทำให้เป็นส่วนตัวและสุดท้ายโดยค่าเริ่มต้นและไม่มีการสร้างตัวตั้งค่า คลาสเองก็ถูกทำให้เป็นขั้นสุดท้ายตามค่าเริ่มต้นเช่นกันเนื่องจากความไม่เปลี่ยนรูปไม่ใช่สิ่งที่สามารถบังคับให้เข้าสู่คลาสย่อยได้ เช่น @Data มีการสร้างเมธอด toString (), equals () และ hashCode () ที่เป็นประโยชน์เช่นกันแต่ละฟิลด์จะได้รับเมธอด getter และตัวสร้างที่ครอบคลุมทุกอาร์กิวเมนต์ (ยกเว้นฟิลด์สุดท้ายที่เริ่มต้นในการประกาศฟิลด์) จะถูกสร้างขึ้นด้วย .
ตัวอย่างการทำงานอย่างสมบูรณ์สามารถพบได้ที่นี่
record
สามารถใช้Java ได้