Tại sao trong Java 8 split đôi khi loại bỏ các chuỗi trống ở đầu mảng kết quả?


110

Trước Java 8 khi chúng ta tách trên chuỗi trống như

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

cơ chế phân chia sẽ phân chia ở những nơi được đánh dấu bằng |

|a|b|c|

vì không gian trống ""tồn tại trước và sau mỗi ký tự. Do đó, lúc đầu nó sẽ tạo ra mảng này

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

và sau đó sẽ xóa các chuỗi trống theo sau (vì chúng tôi đã không cung cấp giá trị âm một cách rõ ràng cho limitđối số) nên cuối cùng nó sẽ trả về

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

Trong Java 8 cơ chế phân chia dường như đã thay đổi. Bây giờ khi chúng ta sử dụng

"abc".split("")

chúng ta sẽ nhận được ["a", "b", "c"]mảng thay vì vì ["", "a", "b", "c"]vậy có vẻ như các chuỗi trống khi bắt đầu cũng bị loại bỏ. Nhưng lý thuyết này không thành công vì ví dụ

"abc".split("a")

trả về mảng có chuỗi trống khi bắt đầu ["", "bc"].

Ai đó có thể giải thích điều gì đang xảy ra ở đây và các quy tắc phân tách đã thay đổi như thế nào trong Java 8 không?


Java8 dường như khắc phục điều đó. Trong khi đó, s.split("(?!^)")dường như hoạt động.
shkschneider

2
@shkschneider Hành vi được mô tả trong câu hỏi của tôi không phải là lỗi của các phiên bản Java-8 trước. Hành vi này không đặc biệt hữu ích, nhưng nó vẫn đúng (như được hiển thị trong câu hỏi của tôi), vì vậy chúng tôi không thể nói rằng nó đã được "sửa". Tôi thấy nó giống như cải thiện vì vậy chúng tôi có thể sử dụng split("")thay vì khó hiểu (đối với những người không sử dụng regex) split("(?!^)")hoặc split("(?<!^)")hoặc những người khác vài regexes.
Pshemo

1
Gặp phải sự cố tương tự sau khi nâng cấp fedora lên Fedora 21, fedora 21 xuất xưởng với JDK 1.8 và ứng dụng trò chơi IRC của tôi bị hỏng vì điều này.
LiuYan 刘 研

7
Câu hỏi này dường như là tài liệu duy nhất về sự thay đổi đột phá này trong Java 8. Oracle đã loại nó ra khỏi danh sách các lỗi không tương thích của họ .
Sean Van Gorder

4
Sự thay đổi này trong JDK chỉ khiến tôi mất 2 giờ để theo dõi xem có gì sai sót. Mã chạy tốt trong máy tính của tôi (JDK8) nhưng bị lỗi bí ẩn trên một máy khác (JDK7). Oracle THỰC SỰ NÊN cập nhật tài liệu của String.split (String regex) , thay vì trong Pattern.split hoặc String.split (String regex, int limit) vì đây là cách sử dụng phổ biến nhất. Java được biết đến với tính di động hay còn gọi là WORA. Đây là một sự thay đổi lớn mang tính đột phá và không được ghi chép đầy đủ.
PoweredByRice

Câu trả lời:


84

Hành vi của String.split(mà gọi Pattern.split) thay đổi giữa Java 7 và Java 8.

Tài liệu

So sánh giữa các tài liệu của Pattern.splittrong Java 7Java 8 , chúng tôi tuân thủ các điều khoản sau đây được bổ sung:

Khi có một kết quả khớp có độ rộng dương ở đầu chuỗi đầu vào thì một chuỗi con trống ở đầu dãy kết quả sẽ được đưa vào. Tuy nhiên, một kết quả có độ rộng bằng 0 ở đầu không bao giờ tạo ra chuỗi con đứng đầu trống như vậy.

Mệnh đề tương tự cũng được thêm vào String.splittrong Java 8 , so với Java 7 .

Thực hiện tham khảo

Hãy để chúng tôi so sánh mã của Pattern.splitphép tham chiếu trong Java 7 và Java 8. Mã được truy xuất từ ​​grepcode, cho phiên bản 7u40-b43 và 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);
}

Java 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);
}

Việc bổ sung mã sau trong Java 8 loại trừ khớp có độ dài bằng 0 ở đầu chuỗi đầu vào, điều này giải thích hành vi ở trên.

            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;
            }

Duy trì khả năng tương thích

Hành vi sau trong Java 8 trở lên

Để thực hiện splithành vi nhất quán giữa các phiên bản và tương thích với hành vi trong Java 8:

  1. Nếu regex của bạn có thể khớp với chuỗi có độ dài bằng 0, chỉ cần thêm (?!\A)vào cuối regex và bọc regex ban đầu trong nhóm không bắt (?:...)(nếu cần).
  2. Nếu regex của bạn không thể khớp với chuỗi có độ dài bằng 0, bạn không cần phải làm gì cả.
  3. Nếu bạn không biết liệu regex có thể khớp với chuỗi độ dài bằng 0 hay không, hãy thực hiện cả hai thao tác trong bước 1.

(?!\A) kiểm tra xem chuỗi không kết thúc ở đầu chuỗi, điều này ngụ ý rằng kết hợp là một kết quả trống ở đầu chuỗi.

Hành vi sau trong Java 7 trở về trước

Không có giải pháp chung nào để splittương thích ngược với Java 7 trở về trước, ngắn gọn là thay thế tất cả các phiên bản của splitđể trỏ đến triển khai tùy chỉnh của riêng bạn.


Bất kỳ ý tưởng nào về cách tôi có thể thay đổi split("")mã để nó nhất quán trên các phiên bản java khác nhau?
Daniel

2
@Daniel: Có thể làm cho nó tương thích về phía trước (theo hành vi của Java 8) bằng cách thêm (?!^)vào cuối regex và bọc regex ban đầu trong nhóm không bắt (?:...)(nếu cần), nhưng tôi không thể nghĩ ra bất kỳ cách làm cho nó tương thích ngược (theo hành vi cũ trong Java 7 trở về trước).
nhahtdh 30/10/15

Cảm ơn vì lời giải thích. Bạn có thể mô tả "(?!^)"? Nó sẽ khác với những tình huống ""nào? (Tôi kinh khủng regex !: - /).
Daniel,

1
@Daniel: Ý nghĩa của nó bị ảnh hưởng bởi Pattern.MULTILINEcờ, trong khi \Aluôn khớp ở đầu chuỗi bất kể cờ.
nhahtdh

30

Điều này đã được chỉ rõ trong tài liệu của split(String regex, limit).

Khi có một kết quả khớp có độ rộng dương ở đầu chuỗi này thì một chuỗi con ở đầu trống sẽ được đưa vào ở đầu mảng kết quả. Tuy nhiên, một kết quả có độ rộng bằng 0 ở đầu không bao giờ tạo ra chuỗi con đứng đầu trống như vậy.

Ở đầu, "abc".split("")bạn có một kết quả khớp không có độ rộng bằng không nên chuỗi con trống ở đầu không được bao gồm trong mảng kết quả.

Tuy nhiên, trong đoạn mã thứ hai của bạn khi bạn tách "a" bạn có một kết quả phù hợp chiều rộng dương (1 trong trường hợp này), vì vậy chuỗi con trống ở đầu được đưa vào như mong đợi.

(Đã xóa mã nguồn không liên quan)


3
Nó chỉ là một câu hỏi. Có thể đăng một đoạn mã từ JDK không? Bạn còn nhớ vấn đề bản quyền với Google - Harry Potter - Oracle?
Paul Vargas

6
@PaulVargas Công bằng mà nói, tôi không biết nhưng tôi cho rằng nó ổn vì bạn có thể tải xuống JDK và giải nén tệp src chứa tất cả các nguồn. Vì vậy, về mặt kỹ thuật, mọi người đều có thể xem nguồn.
Alexis C.

12
@PaulVargas Chữ "mở" trong "mã nguồn mở" đại diện cho một cái gì đó.
Marko Topolnik

2
@ZouZou: chỉ vì mọi người có thể nhìn thấy nó không có nghĩa là bạn có thể xuất bản lại nó
user102008

2
@Paul Vargas, IANAL nhưng trong nhiều trường hợp khác, loại bài đăng này nằm trong tình huống trích dẫn / sử dụng hợp lý. Thông tin thêm về chủ đề có tại đây: meta.stackexchange.com/questions/12527/…
Alex Pakka

14

Có một chút thay đổi trong các tài liệu split()từ Java 7 sang Java 8. Cụ thể, câu lệnh sau đã được thêm vào:

Khi có một kết quả khớp có độ rộng dương ở đầu chuỗi này thì một chuỗi con ở đầu trống sẽ được đưa vào ở đầu mảng kết quả. Tuy nhiên, một kết quả có độ rộng bằng 0 ở đầu không bao giờ tạo ra chuỗi con đứng đầu trống như vậy.

(nhấn mạnh của tôi)

Việc tách chuỗi trống tạo ra một kết quả khớp có độ rộng bằng 0 ở đầu, do đó, một chuỗi trống không được bao gồm ở đầu mảng kết quả theo những gì được chỉ định ở trên. Ngược lại, ví dụ thứ hai của bạn tách "a"ra tạo ra một kết quả khớp có độ rộng dương ở đầu chuỗi, do đó, một chuỗi trống trên thực tế được bao gồm ở đầu mảng kết quả.


Một vài giây nữa đã tạo ra sự khác biệt.
Paul Vargas

2
@PaulVargas thực sự ở đây arshajii đã đăng câu trả lời trước ZouZou vài giây, nhưng tiếc là ZouZou đã trả lời câu hỏi của tôi trước đó ở đây . Tôi đã tự hỏi liệu tôi có nên hỏi câu hỏi này không vì tôi đã biết một câu trả lời nhưng nó có vẻ thú vị và ZouZou xứng đáng nhận được một số danh tiếng cho nhận xét trước đó của mình.
Pshemo

5
Mặc dù hành vi mới trông hợp lý hơn , nhưng rõ ràng đó là một sự phá vỡ khả năng tương thích ngược . Lý do duy nhất cho sự thay đổi này "some-string".split("")là một trường hợp khá hiếm.
ivstas

4
.split("")không phải là cách duy nhất để tách mà không cần khớp với bất cứ thứ gì. Chúng tôi đã sử dụng một regex lookahead tích cực trong jdk7 cũng khớp ở phần đầu và tạo ra một phần tử trống mà hiện đã biến mất. github.com/spray/spray/commit/…
jrudolph
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.