เหตุใดใน Java 8 แยกบางครั้งจึงลบสตริงว่างเมื่อเริ่มต้นอาร์เรย์ผลลัพธ์


110

ก่อน Java 8เมื่อเราแยกสตริงว่างเช่น

String[] tokens = "abc".split("");

กลไกการแยกจะแยกในสถานที่ที่มีเครื่องหมาย |

|a|b|c|

เนื่องจาก""มีพื้นที่ว่างก่อนและหลังอักขระแต่ละตัว ดังนั้นผลลัพธ์มันจะสร้างอาร์เรย์นี้ในตอนแรก

["", "a", "b", "c", ""]

และต่อมาจะลบสตริงว่างต่อท้าย (เนื่องจากเราไม่ได้ระบุค่าลบให้กับlimitอาร์กิวเมนต์อย่างชัดเจน) ดังนั้นในที่สุดก็จะคืนค่า

["", "a", "b", "c"]

ในกลไกการแบ่งJava 8ดูเหมือนจะเปลี่ยนไป ทีนี้เมื่อเราใช้

"abc".split("")

เราจะได้["a", "b", "c"]อาร์เรย์แทน["", "a", "b", "c"]ดังนั้นดูเหมือนว่าสตริงว่างเมื่อเริ่มต้นจะถูกลบออกไปด้วย แต่ทฤษฎีนี้ล้มเหลวเพราะตัวอย่างเช่น

"abc".split("a")

["", "bc"]ส่งกลับอาร์เรย์กับสตริงที่ว่างเปล่าในช่วงเริ่มต้น

ใครช่วยอธิบายได้ว่าเกิดอะไรขึ้นที่นี่และกฎของการแยกเปลี่ยนไปอย่างไรใน Java 8


ดูเหมือนว่า Java8 จะแก้ไขได้ ในขณะเดียวกันs.split("(?!^)")ดูเหมือนว่าจะทำงาน
shkschneider

2
@shkschneider Behavior ที่อธิบายไว้ในคำถามของฉันไม่ใช่ข้อผิดพลาดของ Java-8 เวอร์ชันก่อน พฤติกรรมนี้ไม่ได้มีประโยชน์มากนัก แต่ก็ยังคงถูกต้อง (ดังที่แสดงในคำถามของฉัน) ดังนั้นเราจึงไม่สามารถพูดได้ว่า "แก้ไขแล้ว" ฉันเห็นว่ามันเหมือนกับการปรับปรุงดังนั้นเราจึงสามารถใช้split("")แทนการคลุมเครือ (สำหรับผู้ที่ไม่ใช้ regex) split("(?!^)")หรือsplit("(?<!^)")regexes อื่น ๆ
Pshemo

1
พบปัญหาเดียวกันหลังจากอัปเกรด fedora เป็น Fedora 21, fedora 21 มาพร้อมกับ JDK 1.8 และแอปพลิเคชันเกม IRC ของฉันเสียเพราะเหตุนี้
LiuYan 刘研

7
คำถามนี้ดูเหมือนจะเป็นเอกสารเดียวของการเปลี่ยนแปลงครั้งใหญ่นี้ใน Java 8 ที่ Oracle ทิ้งไว้ในรายการความเข้ากันไม่ได้
Sean Van Gorder

4
การเปลี่ยนแปลงใน JDK นี้ทำให้ฉันเสียเวลา 2 ชั่วโมงในการติดตามสิ่งที่ผิดพลาด รหัสทำงานได้ดีในคอมพิวเตอร์ของฉัน (JDK8) แต่ล้มเหลวอย่างลึกลับในเครื่องอื่น (JDK7) Oracle ควรอัปเดตเอกสารของString.split (String regex)แทนที่จะเป็น Pattern.split หรือ String.split (String regex ขีด จำกัด int) เนื่องจากเป็นการใช้งานทั่วไป Java เป็นที่รู้จักในด้านการพกพาหรือที่เรียกว่า WORA นี่เป็นการเปลี่ยนแปลงที่ล้าหลังครั้งใหญ่และไม่ได้รับการบันทึกไว้เป็นอย่างดี
PoweredByRice

คำตอบ:


84

พฤติกรรมของString.split(ที่เรียกPattern.split) เปลี่ยนไประหว่าง Java 7 และ Java 8

เอกสารประกอบ

เมื่อเปรียบเทียบระหว่างเอกสารของPattern.splitในJava 7และJava 8เราสังเกตว่ามีการเพิ่มประโยคต่อไปนี้:

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

ประโยคเดียวกันถูกเพิ่มไปยังString.splitในJava 8เมื่อเทียบกับJava 7

การใช้งานอ้างอิง

ให้เราเปรียบเทียบรหัสของPattern.splitการใช้การอ้างอิงใน Java 7 และ Java 8 รหัสถูกดึงมาจาก grepcode สำหรับเวอร์ชัน 7u40-b43 และ 8-b132

Java 7

public String[] split(CharSequence input, int limit) {
    int index = 0;
    boolean matchLimited = limit > 0;
    ArrayList<String> matchList = new ArrayList<>();
    Matcher m = matcher(input);

    // Add segments before each match found
    while(m.find()) {
        if (!matchLimited || matchList.size() < limit - 1) {
            String match = input.subSequence(index, m.start()).toString();
            matchList.add(match);
            index = m.end();
        } else if (matchList.size() == limit - 1) { // last one
            String match = input.subSequence(index,
                                             input.length()).toString();
            matchList.add(match);
            index = m.end();
        }
    }

    // If no match was found, return this
    if (index == 0)
        return new String[] {input.toString()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

จาวา 8

public String[] split(CharSequence input, int limit) {
    int index = 0;
    boolean matchLimited = limit > 0;
    ArrayList<String> matchList = new ArrayList<>();
    Matcher m = matcher(input);

    // Add segments before each match found
    while(m.find()) {
        if (!matchLimited || matchList.size() < limit - 1) {
            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }
            String match = input.subSequence(index, m.start()).toString();
            matchList.add(match);
            index = m.end();
        } else if (matchList.size() == limit - 1) { // last one
            String match = input.subSequence(index,
                                             input.length()).toString();
            matchList.add(match);
            index = m.end();
        }
    }

    // If no match was found, return this
    if (index == 0)
        return new String[] {input.toString()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

การเพิ่มโค้ดต่อไปนี้ใน Java 8 จะไม่รวมการจับคู่ความยาวเป็นศูนย์ที่จุดเริ่มต้นของสตริงอินพุตซึ่งจะอธิบายถึงพฤติกรรมข้างต้น

            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }

การรักษาความเข้ากันได้

ติดตามพฤติกรรมใน Java 8 ขึ้นไป

เพื่อให้splitทำงานได้อย่างสม่ำเสมอในทุกเวอร์ชันและเข้ากันได้กับพฤติกรรมใน Java 8:

  1. หาก regex ของคุณสามารถจับคู่สตริงที่มีความยาวเป็นศูนย์ได้ให้เพิ่ม(?!\A)ที่ส่วนท้ายของ regex และรวม regex ดั้งเดิมไว้ในกลุ่มที่ไม่ได้จับภาพ(?:...)(ถ้าจำเป็น)
  2. หาก regex ของคุณไม่สามารถจับคู่สตริงที่มีความยาวเป็นศูนย์ได้คุณไม่จำเป็นต้องทำอะไรเลย
  3. หากคุณไม่ทราบว่า regex สามารถจับคู่สตริงที่มีความยาวเป็นศูนย์ได้หรือไม่ให้ทำทั้งสองอย่างในขั้นตอนที่ 1

(?!\A) ตรวจสอบว่าสตริงไม่ได้สิ้นสุดที่จุดเริ่มต้นของสตริงซึ่งหมายความว่าการจับคู่เป็นการจับคู่ว่างที่จุดเริ่มต้นของสตริง

พฤติกรรมต่อไปนี้ใน Java 7 และก่อนหน้า

ไม่มีวิธีแก้ปัญหาทั่วไปที่จะทำให้splitเข้ากันได้กับ Java 7 และรุ่นก่อนหน้าโดยไม่ต้องแทนที่อินสแตนซ์ทั้งหมดsplitเพื่อชี้ไปที่การนำไปใช้งานที่คุณกำหนดเอง


มีความคิดอย่างไรที่ฉันจะเปลี่ยนsplit("")รหัสเพื่อให้สอดคล้องกันในเวอร์ชัน java ต่างๆ
Daniel

2
@Daniel: เป็นไปได้ที่จะทำให้มันเข้ากันได้ในอนาคต (ทำตามพฤติกรรมของ Java 8) โดยการเพิ่ม(?!^)ที่ส่วนท้ายของ regex และรวม regex ดั้งเดิมไว้ในกลุ่มที่ไม่ได้จับภาพ(?:...)(ถ้าจำเป็น) แต่ฉันคิดไม่ออก วิธีทำให้เข้ากันได้แบบย้อนหลัง (ทำตามพฤติกรรมเก่าใน Java 7 และก่อนหน้า)
nhahtdh

ขอบคุณสำหรับคำอธิบาย คุณช่วยอธิบายได้"(?!^)"ไหม มันจะแตกต่างจากสถานการณ์""ใดบ้าง? (ฉันแย่มากที่ regex!: - /)
Daniel

1
@ แดเนียล: ความหมายของมันได้รับผลกระทบจากPattern.MULTILINEแฟ\Aล็กในขณะที่จับคู่ที่จุดเริ่มต้นของสตริงโดยไม่คำนึงถึงแฟล็ก
nhahtdh

30

สิ่งนี้ได้ระบุไว้ในเอกสารของsplit(String regex, limit).

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

ใน"abc".split("")คุณมีการจับคู่ความกว้างเป็นศูนย์ที่จุดเริ่มต้นดังนั้นสตริงย่อยว่างชั้นนำจึงไม่รวมอยู่ในอาร์เรย์ผลลัพธ์

อย่างไรก็ตามในส่วนย่อยที่สองของคุณเมื่อคุณแยก"a"คุณมีการจับคู่ความกว้างเชิงบวก (1 ในกรณีนี้) ดังนั้นสตริงย่อยนำหน้าว่างจึงรวมอยู่ตามที่คาดไว้

(ลบซอร์สโค้ดที่ไม่เกี่ยวข้องออก)


3
มันเป็นเพียงคำถาม สามารถโพสต์โค้ดส่วนย่อยจาก JDK ได้หรือไม่? จำปัญหาลิขสิทธิ์ของ Google - Harry Potter - Oracle ได้ไหม
Paul Vargas

6
@PaulVargas เพื่อความยุติธรรมฉันไม่รู้ แต่ฉันคิดว่ามันโอเคเพราะคุณสามารถดาวน์โหลด JDK และคลายซิปไฟล์ src ซึ่งมีแหล่งที่มาทั้งหมด ในทางเทคนิคแล้วทุกคนจะเห็นแหล่งที่มา
Alexis C.

12
@PaulVargas "เปิด" ใน "โอเพ่นซอร์ส" หมายถึงบางสิ่งบางอย่าง
Marko Topolnik

2
@ZouZou: เพียงเพราะทุกคนเห็นมันไม่ได้หมายความว่าคุณสามารถเผยแพร่ซ้ำได้
user102008

2
@Paul Vargas, IANAL แต่ในหลาย ๆ ครั้งโพสต์ประเภทนี้อยู่ภายใต้สถานการณ์การอ้าง / การใช้งานที่เหมาะสม เพิ่มเติมในหัวข้อนี้: meta.stackexchange.com/questions/12527/…
Alex Pakka

14

มีการเปลี่ยนแปลงเล็กน้อยในเอกสารสำหรับsplit()จาก Java 7 เป็น Java 8 โดยเฉพาะมีการเพิ่มคำสั่งต่อไปนี้:

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

(เน้นเหมือง)

การแยกสตริงว่างจะสร้างการจับคู่ความกว้างเป็นศูนย์ที่จุดเริ่มต้นดังนั้นสตริงว่างจะไม่รวมอยู่ที่จุดเริ่มต้นของอาร์เรย์ผลลัพธ์ตามที่ระบุไว้ข้างต้น ในทางตรงกันข้ามตัวอย่างที่สองของคุณที่แยกออก"a"จะสร้างการจับคู่ความกว้างบวกที่จุดเริ่มต้นของสตริงดังนั้นในความเป็นจริงสตริงว่างจะรวมอยู่ที่จุดเริ่มต้นของอาร์เรย์ผลลัพธ์


อีกไม่กี่วินาทีก็สร้างความแตกต่าง
Paul Vargas

2
@PaulVargas จริงที่นี่ arshajii คำตอบที่โพสต์ไม่กี่วินาทีก่อน ZouZou แต่โชคร้าย ZouZou ตอบคำถามของฉันก่อนหน้านี้ที่นี่ ฉันสงสัยว่าควรถามคำถามนี้หรือไม่เพราะฉันรู้คำตอบแล้ว แต่ดูเหมือนว่าน่าสนใจและ ZouZou สมควรได้รับชื่อเสียงจากความคิดเห็นก่อนหน้านี้
Pshemo

5
แม้จะมีลักษณะการทำงานใหม่มีลักษณะตรรกะก็จะเห็นได้ชัดแบ่งกันได้ย้อนหลัง เหตุผลเดียวสำหรับการเปลี่ยนแปลงนี้"some-string".split("")คือเป็นกรณีที่ค่อนข้างหายาก
ivstas

4
.split("")ไม่ใช่วิธีเดียวที่จะแยกโดยไม่จับคู่อะไรเลย เราใช้ regex lookahead ที่เป็นบวกซึ่งใน jdk7 ซึ่งตรงกับตอนต้นและสร้างองค์ประกอบส่วนหัวที่ว่างเปล่าซึ่งตอนนี้หายไปแล้ว github.com/spray/spray/commit/…
jrudolph
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.