เปรียบเทียบ pdf java – วิธีเปรียบเทียบไฟล์ PDF ใน Java อย่างโปรแกรมมิ่ง

หากคุณเป็นนักพัฒนา Java ที่ต้องการ compare pdf java อย่างรวดเร็วและแม่นยำ คุณมาถูกที่แล้ว ไม่ว่าคุณจะกำลังสร้างระบบจัดการเนื้อหา (content‑management system) เพิ่มการควบคุมเวอร์ชันให้กับสัญญากฎหมาย หรือทำอัตโนมัติ QA สำหรับรายงานที่สร้างขึ้น การตรวจสอบแบบข้างเคียงด้วยตนเองนั้นเปี่ยมด้วยข้อผิดพลาดและใช้เวลามาก GroupDocs.Comparison for Java ให้ API เดียวที่เชื่อถือได้ซึ่งตรวจจับการแทรก การลบ การเปลี่ยนแปลงรูปแบบ และแม้กระทั่งย่อหน้าที่ย้ายตำแหน่ง — ทั้งหมดนี้โดยไม่ต้องเขียนตรรกะ diff ซับซ้อนด้วยตนเอง

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

คำตอบสั้น

  • What library lets me compare PDF files in Java? GroupDocs.Comparison for Java.
  • Do I need a license? A free trial works for learning; a full license is required for production.
  • Which Java version is required? Java 8 minimum, Java 11+ recommended.
  • Can I compare documents without saving them to disk? Yes – use InputStream‑based overloads to keep everything in memory.
  • How do I get change coordinates? Call setCalculateCoordinates(true) on CompareOptions before invoking compare.

วิธีเปรียบเทียบไฟล์ PDF ใน Java (compare pdf java)?

โหลด PDF สองไฟล์ด้วยอินสแตนซ์ Comparer, ตั้งค่า CompareOptions ตามต้องการ, แล้วเรียก compare เมธอดจะคืนค่าอาเรย์ ChangeInfo[] ที่บอกว่ามีอะไรเปลี่ยนแปลง, ที่ไหน, และอย่างไร ทั้งกระบวนการทั้งหมดสามารถเขียนได้ในไม่ถึงสิบบรรทัดของ Java และห้องสมุดจะดูแลเรื่องความแตกต่างตามรูปแบบให้คุณโดยอัตโนมัติ

“compare pdf files java” คืออะไร?

วลี compare pdf files java หมายถึงกระบวนการโปรแกรมมิ่งในการวิเคราะห์เอกสาร PDF (หรือรูปแบบที่รองรับ) สองไฟล์ในแอปพลิเคชัน Java เพื่อสร้าง diff รายละเอียด diff จะรวมถึงข้อความ, รูปภาพ, ตารางที่ถูกแทรก, ลบ, หรือแก้ไข, รวมถึงส่วนที่ย้ายตำแหน่ง โดยจัดเป็นรายการโครงสร้างที่สามารถเรนเดอร์, บันทึก, หรือส่งต่อไปยังบริการ downstream ได้

ทำไมต้องใช้ GroupDocs.Comparison for Java?

GroupDocs.Comparison รองรับรูปแบบเข้าและออกกว่า 60 รูปแบบ รวมถึง PDF, DOCX, XLSX, PPTX, HTML, และรูปภาพ พร้อมรักษาเลย์เอาต์ไว้ครบถ้วน สามารถประมวลผลไฟล์หลายร้อยหน้าโดยไม่ต้องโหลดเอกสารทั้งหมดเข้าสู่หน่วยความจำ ให้ผลลัพธ์ภายในไม่กี่วินาทีสำหรับ PDF 50 หน้าแบบทั่วไป ตัวเลือกในตัวช่วยให้คุณละเลยการเปลี่ยนแปลงสไตล์, ตรวจจับเนื้อหาที่ย้าย, และคำนวณพิกัดหน้าสำหรับแต่ละการเปลี่ยนแปลง

วิธีเปรียบเทียบไฟล์ PDF ด้วยโปรแกรมใน Java

ด้านล่างเป็นกระบวนการแบบ end‑to‑end ที่คุณจะทำตามในโปรเจกต์ของคุณ แต่ละขั้นตอนจะอธิบายก่อน placeholder ที่สอดคล้องกัน เพื่อให้คุณรู้เหตุผลที่โค้ดนั้นอยู่ที่นั่นเสมอ

ข้อกำหนดเบื้องต้นและสิ่งที่คุณต้องการ

  • Java Development Kit (JDK) – เวอร์ชัน 8 หรือสูงกว่า (Java 11+ ให้การจัดการ garbage‑collection และโมดูลที่ดีกว่า)
  • IDE – IntelliJ IDEA, Eclipse, หรือเครื่องมือแก้ไขใด ๆ ที่รองรับ Maven
  • Maven – สำหรับการจัดการ dependency; ตัวอย่างใช้ pom.xml มาตรฐานของ Maven
  • Sample documents – PDF สองไฟล์ (หรือรูปแบบที่รองรับ) ที่มีความแตกต่างเล็กน้อยสำหรับการทดสอบ

ตั้งค่า GroupDocs.Comparison for Java

การกำหนดค่า Maven

แรกเริ่มให้เพิ่มรีโพสิตอรีของ GroupDocs และ dependency ลงใน pom.xml ของคุณ คงบล็อกไว้ตามที่แสดง:

<repositories>
   <repository>
      <id>repository.groupdocs.com</id>
      <name>GroupDocs Repository</name>
      <url>https://releases.groupdocs.com/comparison/java/</url>
   </repository>
</repositories>

<dependencies>
   <dependency>
      <groupId>com.groupdocs</groupId>
      <artifactId>groupdocs-comparison</artifactId>
      <version>25.2</version>
   </dependency>
</dependencies>

Pro Tip: ตรวจสอบให้แน่ใจว่าคุณใช้เวอร์ชันเสถียรล่าสุดจากหน้าดาวน์โหลดของ GroupDocs เวอร์ชันใหม่มักเพิ่มการสนับสนุนรูปแบบเพิ่มเติมและปรับปรุงประสิทธิภาพ

ปัญหาการตั้งค่าทั่วไปและวิธีแก้

  • “Repository not found” – ตรวจสอบให้แน่ใจว่าองค์ประกอบ <repositories> ปรากฏ ก่อน <dependencies>
  • “ClassNotFoundException” – รันการรีเฟรช Maven (เช่น Maven → Reload project ใน IntelliJ) เพื่อดึง JAR เข้าสู่ classpath

อธิบายตัวเลือกไลเซนส์

  1. Free Trial – เหมาะสำหรับการเรียนรู้และสาธิตขนาดเล็ก
  2. Temporary License – ขอคีย์ 30‑วันสำหรับการประเมินระยะยาว
  3. Full License – จำเป็นสำหรับการผลิต, ไม่จำกัดขนาดไฟล์, และรับการสนับสนุนระดับ priority

โครงสร้างโปรเจกต์พื้นฐาน

your-project/
├── src/main/java/
│   └── com/yourcompany/comparison/
│       └── DocumentComparison.java
├── src/test/resources/
│   ├── source.docx
│   └── target.docx
└── pom.xml

การนำไปใช้หลัก: คู่มือขั้นตอน‑โดย‑ขั้นตอน

ทำความเข้าใจคลาส Comparer

คลาส Comparer เป็นจุดเข้าหลักสำหรับการดำเนินการเปรียบเทียบทั้งหมดใน GroupDocs.Comparison

import com.groupdocs.comparison.Comparer;

try (Comparer comparer = new Comparer("sourceFilePath")) {
    comparer.add("targetFilePath");
    // Your comparison logic goes here
}

Why use try‑with‑resources? เนื่องจาก Comparer implements AutoCloseable รูปแบบนี้รับประกันว่าทรัพยากรเนทีฟ (บัฟเฟอร์หน่วยความจำ, ไฟล์ชั่วคราว) จะถูกปล่อยโดยอัตโนมัติ ป้องกันการรั่วไหลของหน่วยความจำเมื่อประมวลผล PDF ขนาดใหญ่

ฟีเจอร์ 1: ดึงพิกัดการเปลี่ยนแปลง

ฟีเจอร์นี้คืนค่าพิกัด X/Y ระดับหน้าอย่างแม่นยำสำหรับการเปลี่ยนแปลงแต่ละรายการ ช่วยให้คุณสร้าง visual diff viewer ได้

เมื่อใดควรใช้
  • สร้างเว็บรีวิวเอกสารที่ไฮไลท์การแก้ไข
  • สร้างบันทึก audit ที่ระบุตำแหน่งการแก้ไขแต่ละรายการ
  • ผสานกับ PDF viewer ที่รองรับการวาง annotation overlay
รายละเอียดการทำงาน
import com.groupdocs.comparison.Comparer;
import com.groupdocs.comparison.result.ChangeInfo;

String sourceFilePath = "path/to/source.docx";
String targetFilePath = "path/to/target.docx";

try (Comparer comparer = new Comparer(sourceFilePath)) {
    // Add the target document for comparison.
    comparer.add(targetFilePath);

CompareOptions กำหนดพฤติกรรมการเปรียบเทียบ เช่น เปิดใช้งานการคำนวณพิกัด

เปิดการคำนวณพิกัด:

import com.groupdocs.comparison.options.CompareOptions;

final Path resultPath = comparer.compare(
        new CompareOptions.Builder()
                .setCalculateCoordinates(true)
                .build());

ดึงและทำงานกับข้อมูลการเปลี่ยนแปลง:

ChangeInfo[] changes = comparer.getChanges();
for (ChangeInfo change : changes) {
    System.out.printf("Change Type: %s, X: %f, Y: %f, Text: %s%n",
            change.getType(), change.getBox().getX(), change.getBox().getY(), change.getText());
}

Performance Note: การเปิดใช้งานพิกัดจะเพิ่มภาระประมาณ 15‑20 %; ปิดเมื่อทำ bulk diff ที่ไม่ต้องการข้อมูลตำแหน่ง

ฟีเจอร์ 2: ดึงการเปลี่ยนแปลงจากเส้นทางไฟล์

หากคุณต้องการเพียงรายการของสิ่งที่เปลี่ยนแปลง วิธีนี้จะคืนค่า ChangeInfo[] ที่มีน้ำหนักเบาโดยไม่มีพิกัด

ChangeInfo แทนการเปลี่ยนแปลงเดี่ยวหนึ่งรายการ รวมประเภทและตำแหน่ง

เหมาะสำหรับ
  • สร้างสรุปการเปลี่ยนแปลงเป็นข้อความธรรมดา
  • รันงาน batch รายคืนที่เปรียบเทียบคู่เอกสารหลายพันคู่
  • ตรวจสอบอย่างรวดเร็วว่ารุ่นสองเวอร์ชันเหมือนกันหรือไม่
การทำงาน
try (Comparer comparer = new Comparer(sourceFilePath)) {
    comparer.add(targetFilePath);

รันการเปรียบเทียบโดยไม่มีตัวเลือกพิเศษ:

final Path resultPath = comparer.compare();
ChangeInfo[] changes = comparer.getChanges();
System.out.println("\nCount of changes: " + changes.length);
}

Best Practice: ตรวจสอบ changes.length เสมอ หากอาเรย์ว่างหมายความว่าเอกสารสองไฟล์เหมือนกัน สามารถข้ามขั้นตอน downstream ได้

ฟีเจอร์ 3: ทำงานกับสตรีม

สตรีมทำให้คุณเปรียบเทียบไฟล์ที่อยู่ในหน่วยความจำ, แชร์เครือข่าย, หรือคลาวด์สตอเรจโดยไม่ต้องสัมผัสไฟล์ระบบท้องถิ่น

กรณีใช้งานทั่วไป
  • รับไฟล์อัปโหลดในคอนโทรลเลอร์ Spring Boot แล้วเปรียบเทียบแบบ on‑the‑fly
  • ดึง PDF จาก AWS S3, Azure Blob, หรือ Google Cloud Storage เข้า ByteArrayInputStream โดยตรง
  • เปรียบเทียบเอกสารที่เก็บในคอลัมน์ BLOB ของฐานข้อมูล
การทำงานกับสตรีม
import java.io.FileInputStream;
import java.io.InputStream;

try (InputStream sourceStream = new FileInputStream(sourceFilePath);
     InputStream targetStream = new FileInputStream(targetFilePath);
     Comparer comparer = new Comparer(sourceStream)) {
    comparer.add(targetStream);

ดำเนินการเปรียบเทียบด้วยเมธอดเดียวกัน:

final Path resultPath = comparer.compare();
ChangeInfo[] changes = comparer.getChanges();
System.out.println("\nCount of changes: " + Arrays.toString(changes).length);
}

Memory Tip: บล็อก try‑with‑resources ทำให้สตรีมปิดอัตโนมัติ ซึ่งสำคัญเมื่อจัดการ PDF ขนาดใหญ่หลายไฟล์ในบริการหลายเธรด

ฟีเจอร์ 4: ดึงข้อความเป้าหมาย

บางครั้งคุณต้องการสแนปของข้อความที่ถูกเพิ่มหรือถูกลบ เพื่อแจ้งเตือนอีเมลหรือบันทึกการเปลี่ยนแปลง

การประยุกต์ใช้จริง
  • ส่งอีเมลแจ้งเตือนที่รวมย่อหน้าที่แทรกเข้ามา
  • เติม UI grid ที่แสดงข้อความ “เก่า vs. ใหม่” ข้างเคียงกัน
  • ตรวจสอบเอกสารกฎระเบียบเพื่อหาการเปลี่ยนแปลงวลีเฉพาะ
การทำงาน
try (Comparer comparer = new Comparer(sourceFilePath)) {
    comparer.add(targetFilePath);
    
    final Path resultPath = comparer.compare();
    ChangeInfo[] changes = comparer.getChanges();

    for (ChangeInfo change : changes) {
        String text = change.getText();
        System.out.println(text);
    }
}

Filtering Tip: ใช้ ChangeInfo.getChangeType() เพื่อกรองเฉพาะการแทรก (INSERT) หรือการลบ (DELETE) เท่านั้น

ปัญหาทั่วไปและวิธีหลีกเลี่ยง

1. ปัญหาเส้นทางไฟล์

Problem: “File not found” แม้ไฟล์จะมีอยู่จริง
Solution: ใช้เส้นทางแบบ absolute ระหว่างการพัฒนา หรือยืนยันไดเรกทอรีทำงานของ IDE บน Windows ให้ escape backslashes (\\) หรือใช้ forward slashes (/)

// Good
String path = "C:/Users/yourname/documents/test.docx";
// Or
String path = "C:\\Users\\yourname\\documents\\test.docx";

2. การรั่วไหลของหน่วยความจำกับไฟล์ขนาดใหญ่

Problem: OutOfMemoryError เมื่อเปรียบเทียบ PDF 200‑หน้า
Solution: ห่อ Comparer ด้วย try‑with‑resources เสมอและใช้ overload แบบสตรีมที่เก็บเฉพาะหน้าที่จำเป็นในหน่วยความจำ

3. รูปแบบไฟล์ที่ไม่รองรับ

Problem: เกิด exception กับรูปแบบ legacy บางประเภท
Solution: ตรวจสอบรายการ supported‑formats อย่างเป็นทางการ (GroupDocs รองรับ 60+ รูปแบบ) หากรูปแบบไม่อยู่ในรายการ ให้แปลงเป็น PDF หรือ DOCX ก่อนเปรียบเทียบ

4. ปัญหาประสิทธิภาพ

Problem: การเปรียบเทียบใช้เวลานานกว่าที่คาด
Solution:

  • ปิดการคำนวณพิกัดหากไม่ต้องการ
  • ใช้ CompareOptions.setDetectMovedBlocks(true) เฉพาะเมื่อต้องการตรวจจับบล็อกที่ย้าย
  • ทำ parallel งานเปรียบเทียบอิสระด้วย thread pool

เคล็ดลับการปรับประสิทธิภาพ

เลือกตัวเลือกที่เหมาะสม

CompareOptions options = new CompareOptions.Builder()
    .setCalculateCoordinates(false) // Only enable when needed
    .setDetectStyleChanges(false)   // Skip formatting if you only care about content
    .build();

การจัดการหน่วยความจำ

  • ประมวลผลเอกสารเป็น batch แทนการโหลดทั้งหมดพร้อมกัน
  • ใช้ API สตรีมสำหรับไฟล์ที่ใหญ่กว่า 50 MB
  • พึ่งพา try‑with‑resources เพื่อรับประกันการทำความสะอาด

กลยุทธ์การแคช

// Pseudo-code for caching concept
String cacheKey = generateCacheKey(sourceFile, targetFile);
if (cache.contains(cacheKey)) {
    return cache.get(cacheKey);
}

สถานการณ์จริงและวิธีแก้

Scenario 1: ระบบจัดการเนื้อหา

public class ArticleVersionComparison {
    public List<ChangeInfo> compareVersions(String oldVersion, String newVersion) {
        try (Comparer comparer = new Comparer(oldVersion)) {
            comparer.add(newVersion);
            final Path result = comparer.compare();
            return Arrays.asList(comparer.getChanges());
        } catch (Exception e) {
            log.error("Failed to compare article versions", e);
            return Collections.emptyList();
        }
    }
}

Scenario 2: การตรวจสอบคุณภาพอัตโนมัติ

public boolean validateReportAgainstTemplate(InputStream report, InputStream template) {
    try (Comparer comparer = new Comparer(template)) {
        comparer.add(report);
        comparer.compare();
        ChangeInfo[] changes = comparer.getChanges();
        
        // Only allow certain types of changes
        return Arrays.stream(changes)
                .allMatch(change -> isAllowedChange(change));
    } catch (Exception e) {
        return false;
    }
}

Scenario 3: การประมวลผลเอกสารเป็นชุด

public void processBatchComparison(List<DocumentPair> documents) {
    documents.parallelStream().forEach(pair -> {
        try (Comparer comparer = new Comparer(pair.getSource())) {
            comparer.add(pair.getTarget());
            Path result = comparer.compare();
            // Process results...
        } catch (Exception e) {
            log.error("Failed to process document pair: " + pair, e);
        }
    });
}

ฟีเจอร์ขั้นสูงและแนวปฏิบัติที่ดีที่สุด

ทำงานกับรูปแบบไฟล์ต่าง ๆ

public boolean isFormatSupported(String filePath) {
    String extension = getFileExtension(filePath);
    List<String> supportedFormats = Arrays.asList(
        ".docx", ".pdf", ".txt", ".rtf", ".odt", // Add more as needed
    );
    return supportedFormats.contains(extension.toLowerCase());
}

จัดการเอกสารขนาดใหญ่

CompareOptions largeDocOptions = new CompareOptions.Builder()
    .setCalculateCoordinates(false)  // Saves memory
    .setDetectStyleChanges(false)    // Focuses on content only
    .setWordsLimit(1000)             // Limits processing scope
    .build();

รูปแบบการจัดการข้อผิดพลาด

public ComparisonResult compareDocuments(String source, String target) {
    try (Comparer comparer = new Comparer(source)) {
        comparer.add(target);
        Path result = comparer.compare();
        
        return ComparisonResult.success(comparer.getChanges());
        
    } catch (SecurityException e) {
        log.error("Access denied when comparing documents", e);
        return ComparisonResult.failure("Access denied");
    } catch (IOException e) {
        log.error("IO error during document comparison", e);
        return ComparisonResult.failure("File access error");
    } catch (Exception e) {
        log.error("Unexpected error during comparison", e);
        return ComparisonResult.failure("Comparison failed");
    }
}

คำถามที่พบบ่อย

Q: เวอร์ชัน Java ขั้นต่ำที่ต้องการสำหรับ GroupDocs.Comparison คืออะไร?
A: Java 8 เป็นเวอร์ชันขั้นต่ำที่รองรับ; แนะนำให้ใช้ Java 11+ เพื่อประสิทธิภาพการจัดการ garbage collection และโมดูลที่ดีขึ้น

Q: ฉันสามารถเปรียบเทียบเอกสารมากกว่าสองไฟล์พร้อมกันได้หรือไม่?
A: GroupDocs.Comparison เปรียบเทียบคู่เดียวต่อครั้ง สำหรับการเวอร์ชันหลายเอกสาร ให้วนลูปผ่านรายการเอกสารและเปรียบเทียบแต่ละคู่ต่อเนื่อง เก็บ ChangeInfo[] ที่ได้ไว้สำหรับการสรุปภายหลัง

try (Comparer comparer = new Comparer(sourceDocument)) {
    comparer.add(targetDocument1);
    comparer.add(targetDocument2);
    comparer.add(targetDocument3);
    // Now compare against all targets
}

Q: ควรจัดการกับเอกสารขนาดใหญ่มาก (100 MB+) อย่างไร?
A:

  • ปิดการคำนวณพิกัดหากไม่ต้องการตำแหน่งที่แม่นยำ
  • ใช้ API แบบสตรีมเพื่อหลีกเลี่ยงการโหลดไฟล์ทั้งหมดเข้าสู่ RAM
  • แบ่งการประมวลผลเป็นช่วงหน้า หากต้องการเปลี่ยนแปลงเฉพาะส่วนหนึ่ง
  • ตรวจสอบการใช้ heap ของ JVM และปรับ -Xmx ให้เหมาะสม

Q: มีวิธีไฮไลท์การเปลี่ยนแปลงในผลลัพธ์แบบ visual หรือไม่?
A: มี หลังจากได้ ChangeInfo[] คุณสามารถสร้าง PDF ใหม่โดยใช้ GroupDocs.Watermark หรือไลบรารี PDF ใด ๆ วาดสี่เหลี่ยมที่พิกัดที่คืนมา ผลลัพธ์จะเป็นเวอร์ชัน “red‑line” ที่ผู้ใช้สุดท้ายสามารถรีวิวใน PDF viewer ใดก็ได้

CompareOptions options = new CompareOptions.Builder()
    .setShowInsertedContent(true)
    .setShowDeletedContent(true)
    .setGenerateOutputDocument(true)
    .build();

Q: จะจัดการกับเอกสารที่มีรหัสผ่านอย่างไร?
A: ส่งรหัสผ่านไปยังคอนสตรัคเตอร์ Comparer หรือกำหนดบนอ็อบเจกต์ LoadOptions ก่อนเรียก compare ไลบรารีจะถอดรหัสเอกสารในหน่วยความจำ ดังนั้นรหัสผ่านจะไม่ต้องสัมผัสไฟล์ระบบ

LoadOptions loadOptions = new LoadOptions();
loadOptions.setPassword("your-password");

try (Comparer comparer = new Comparer(protectedDocument, loadOptions)) {
    // Comparison logic here
}

Q: ฉันสามารถปรับแต่งวิธีการตรวจจับการเปลี่ยนแปลงได้หรือไม่?
A: แน่นอน CompareOptions มีฟลักเช่น setIgnoreFormatting(true), setDetectMovedBlocks(true), และ setGranularity(Granularity.WORD) ปรับให้ตรงกับกฎธุรกิจของคุณ — เช่น ละเลยการเปลี่ยนแปลงฟอนต์แต่ยังตรวจจับย่อหน้าที่ย้าย

CompareOptions options = new CompareOptions.Builder()
    .setDetectStyleChanges(false)     // Ignore formatting changes
    .setSensitivityOfComparison(100)  // Adjust sensitivity (0‑100)
    .build();

Q: วิธีที่ดีที่สุดในการผสานรวมกับ Spring Boot คืออะไร?
A: สร้าง bean @Service ที่ฉีดพาธไลเซนส์ แล้วเปิด endpoint @RestController ที่รับอัปโหลด MultipartFile ภายในคอนโทรลเลอร์แปลง MultipartFile เป็น InputStream แล้วเรียกเมธอดเปรียบเทียบแบบสตรีม ส่งคืน ChangeInfo[] เป็น JSON เพื่อให้ front‑end แสดงผล

@Service
public class DocumentComparisonService {
    
    public ComparisonResult compare(MultipartFile source, MultipartFile target) {
        // Implementation using the techniques from this guide
    }
}

แหล่งข้อมูลเพิ่มเติม


อัปเดตล่าสุด: 2026-06-15
ทดสอบด้วย: GroupDocs.Comparison 25.2 for Java
ผู้เขียน: GroupDocs


for (ChangeInfo change : changes) {
    if (change.getType() == ComparisonAction.INSERT) {
        System.out.println("Added: " + change.getText());
    }
}

บทเรียนที่เกี่ยวข้อง