Java 8 split에서 때때로 결과 배열의 시작 부분에서 빈 문자열을 제거하는 이유는 무엇입니까?


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")

start에 빈 문자열이있는 배열을 반환합니다 ["", "bc"].

누군가 여기서 무슨 일이 일어나고 있으며 Java 8에서 분할 규칙이 어떻게 변경되었는지 설명 할 수 있습니까?


Java8이이를 수정하는 것 같습니다. 한편, s.split("(?!^)")작동하는 것 같습니다.
shkschneider

2
내 질문에 설명 된 @shkschneider 동작은 Java-8 이전 버전의 버그가 아닙니다. 이 동작은 그다지 유용하지는 않았지만 여전히 정확했기 때문에 (제 질문에서 볼 수 있듯이) "고정"되었다고 말할 수 없습니다. 나는 그것을 개선과 비슷하게 보아 split("")(정규식을 사용하지 않는 사람들을 위해) split("(?!^)")또는 split("(?<!^)")다른 정규식 대신 사용할 수 있습니다 .
Pshemo 2014 년

1
fedora를 Fedora 21로 업그레이드 한 후 동일한 문제가 발생했으며, fedora 21은 JDK 1.8과 함께 제공되며 이로 인해 IRC 게임 응용 프로그램이 손상되었습니다.
LiuYan 刘 研

7
이 질문은 Java 8의이 주요 변경 사항에 대한 유일한 문서 인 것 같습니다. Oracle 은 비 호환성 목록에서 제외했습니다 .
Sean Van Gorder

4
JDK의 이러한 변경으로 인해 무엇이 잘못되었는지 추적하는 데 2 ​​시간이 소요되었습니다. 내 컴퓨터 (JDK8)에서는 코드가 제대로 실행되지만 다른 컴퓨터 (JDK7)에서는 이상하게 실패합니다. 오라클은 정말 안된다 의 문서 업데이트 사항 String.split (문자열 정규식) 이 지금까지 가장 일반적으로 사용하는 것입니다으로 오히려 Pattern.split 또는 사항 String.split보다, (문자열 정규식, INT 제한). Java는 이른바 WORA라고하는 이식성으로 유명합니다. 이것은 주요 역방향 변경 사항이며 전혀 문서화되어 있지 않습니다.
PoweredByRice

답변:


84

String.split(를 호출하는 Pattern.split) 동작은 Java 7과 Java 8간에 변경됩니다.

선적 서류 비치

의 문서 사이의 비교 Pattern.split자바 (7)자바 (8) , 우리는 다음과 같은 조항을 준수하는 것은 추가되는 :

입력 시퀀스의 시작 부분에 양의 너비가 일치하는 경우 결과 배열의 시작 부분에 빈 선행 부분 문자열이 포함됩니다. 그러나 처음에 너비가 0 인 일치는 이러한 빈 선행 하위 문자열을 생성하지 않습니다.

같은 절은 또한 추가 String.split자바 (8) 에 비해, 자바 7 .

참조 구현

Pattern.splitJava 7과 Java 8에서 참조 구현 코드를 비교해 보겠습니다 . 코드는 버전 7u40-b43 및 8-b132에 대해 grepcode에서 검색됩니다.

자바 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에서 다음 코드를 추가하면 위의 동작을 설명하는 입력 문자열의 시작 부분에서 길이가 0 인 일치 항목이 제외됩니다.

            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지속적으로 자바 8의 행동 버전에서 호환 동작합니다을 :

  1. 당신의 정규식이 경우 수있는 길이가 0 인 문자열과 일치, 다만 추가 (?!\A)마지막 정규식의 비 캡처 그룹의 원래 정규식 포장 (?:...)(필요한 경우).
  2. 정규식 길이가 0 인 문자열과 일치 할 수없는 경우 아무것도 할 필요가 없습니다.
  3. 정규식이 길이가 0 인 문자열과 일치 할 수 있는지 여부를 모르는 경우 1 단계의 두 작업을 모두 수행하십시오.

(?!\A) 문자열이 문자열의 시작 부분에서 끝나지 않는지 확인합니다. 이는 일치 항목이 문자열의 시작 부분에서 비어 있음을 의미합니다.

Java 7 및 이전 버전에서 다음 동작

splitJava 7 및 이전 버전과의 역 호환성 을 만드는 일반적인 솔루션은 없습니다. 모든 인스턴스를 split사용자 정의 구현 으로 바꾸는 것보다 부족 합니다.


split("")다른 Java 버전에서 일관되도록 코드를 어떻게 변경할 수 있습니까?
다니엘

2
@ 다니엘 : 정규식 의 끝에 추가 (?!^)하고 (필요한 경우) 원래 정규식을 비 캡처 그룹으로 래핑하여 ( Java 8의 동작을 따름) 순방향 호환이 가능하도록 만들 수 있지만 이전 버전과 호환되도록 만드는 방법 (Java 7 이하의 이전 동작을 따릅니다). (?:...)
nhahtdh

설명 해주셔서 감사합니다. 설명해 주 "(?!^)"시겠습니까? 어떤 시나리오에서 다른 ""가요? (정규식이 끔찍합니다! :-/).
다니엘

1
@Daniel : 그 의미는 Pattern.MULTILINE플래그의 영향을받는 반면 \A플래그에 관계없이 항상 문자열의 시작 부분에서 일치합니다.
nhahtdh

30

이것은의 문서에 지정되어 split(String regex, limit)있습니다.

이 문자열의 시작 부분에 양의 너비가 일치하면 결과 배열의 시작 부분에 빈 선행 부분 문자열이 포함됩니다. 그러나 처음에 너비가 0 인 일치는 이러한 빈 선행 하위 문자열을 생성하지 않습니다.

에서 "abc".split("")선도적 인 빈 문자열이 결과 배열에 포함되지 않습니다 있도록 시작 부분에 폭 제로 일치를 얻었다.

그러나 분할 할 때 두 번째 스 니펫 "a"에서 양의 너비 일치 (이 경우 1)를 얻었으므로 빈 선행 하위 문자열이 예상대로 포함됩니다.

(관련없는 소스 코드 제거)


3
질문 일뿐입니다. JDK에서 코드 조각을 게시해도됩니까? Google-Harry Potter-Oracle의 저작권 문제를 기억하십니까?
Paul Vargas

6
@PaulVargas 공정하게 말하면 모르겠지만 JDK를 다운로드하고 모든 소스가 포함 된 src 파일의 압축을 풀 수 있으므로 괜찮다고 가정합니다. 기술적으로는 누구나 소스를 볼 수 있습니다.
Alexis C.

12
@PaulVargas "오픈 소스"의 "오픈"은 무언가를 의미합니다.
Marko Topolnik 2014 년

2
@ZouZou : 모두가 볼 수 있다고해서 다시 게시 할 수있는 것은 아닙니다.
user102008

2
@Paul Vargas, IANAL 그러나 다른 많은 경우에 이러한 유형의 게시물은 견적 / 공정 사용 상황에 해당합니다. 주제에 더는 여기에 있습니다 : meta.stackexchange.com/questions/12527/...
알렉스 Pakka

14

문서가 약간 변경되었습니다. split()Java 7에서 Java 8로 특히 다음 명령문이 추가되었습니다.

이 문자열의 시작 부분에 양의 너비가 일치하면 결과 배열의 시작 부분에 빈 선행 부분 문자열이 포함됩니다. 그러나 처음에 너비가 0 인 일치는 이러한 빈 선행 하위 문자열을 생성하지 않습니다.

(강조 내)

빈 문자열 분할은 처음에 너비가 0 인 일치를 생성하므로 위에 지정된 내용에 따라 결과 배열의 시작 부분에 빈 문자열이 포함되지 않습니다. 반대로 분할하는 두 번째 예제 는 문자열의 시작 부분에 양의 너비 일치를 "a"생성 하므로 실제로 결과 배열의 시작 부분에 빈 문자열이 포함됩니다.


몇 초 만에 차이가 생겼습니다.
Paul Vargas

2
@PaulVargas 실제로 여기 arshajii는 ZouZou보다 몇 초 전에 답변을 게시했지만 불행히도 ZouZou는 여기에서 내 질문에 앞서 대답 했습니다 . 나는 이미 답을 알고 있었기 때문에이 질문을해야하는지 궁금했다. 그러나 그것은 흥미로워 보였고 ZouZou는 그의 이전 논평에 대해 어느 정도 명성을 얻을 자격이 있었다.
Pshemo 2014 년

5
새로운 동작이보다 논리적으로 보이지만 분명히 이전 버전과의 호환성이 깨 졌습니다. 이 변경에 대한 유일한 이유 "some-string".split("")는 매우 드문 경우입니다.
ivstas 2014 년

4
.split("")일치하지 않고 분할하는 유일한 방법은 아닙니다. 우리는 jdk7에서 처음에 일치하는 긍정적 인 미리보기 정규식을 사용했으며 이제는 사라진 빈 헤드 요소를 생성했습니다. github.com/spray/spray/commit/…
jrudolph
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.