changeset 11557:b68f6b84dc94

8071479: Stream and lambdafication improvements to j.u.regex.Matcher Reviewed-by: smarks, briangoetz, sherman
author psandoz
date Tue, 03 Mar 2015 12:30:48 +0100
parents ae7e32b84770
children bdb66e8883e2
files src/java.base/share/classes/java/util/regex/Matcher.java test/java/util/regex/PatternStreamTest.java test/java/util/regex/RegExTest.java
diffstat 3 files changed, 577 insertions(+), 114 deletions(-) [+]
line wrap: on
line diff
--- a/src/java.base/share/classes/java/util/regex/Matcher.java	Tue Mar 03 10:30:44 2015 +0300
+++ b/src/java.base/share/classes/java/util/regex/Matcher.java	Tue Mar 03 12:30:48 2015 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1999, 2015, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,16 @@
 
 package java.util.regex;
 
+import java.util.ConcurrentModificationException;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
 import java.util.Objects;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
 /**
  * An engine that performs match operations on a {@linkplain java.lang.CharSequence
@@ -209,6 +218,11 @@
     boolean anchoringBounds = true;
 
     /**
+     * Number of times this matcher's state has been modified
+     */
+    int modCount;
+
+    /**
      * No default constructor.
      */
     Matcher() {
@@ -248,11 +262,76 @@
      * @since 1.5
      */
     public MatchResult toMatchResult() {
-        Matcher result = new Matcher(this.parentPattern, text.toString());
-        result.first = this.first;
-        result.last = this.last;
-        result.groups = this.groups.clone();
-        return result;
+        return toMatchResult(text.toString());
+    }
+
+    private MatchResult toMatchResult(String text) {
+        return new ImmutableMatchResult(this.first,
+                                        this.last,
+                                        groupCount(),
+                                        this.groups.clone(),
+                                        text);
+    }
+
+    private static class ImmutableMatchResult implements MatchResult {
+        private final int first;
+        private final int last;
+        private final int[] groups;
+        private final int groupCount;
+        private final String text;
+
+        ImmutableMatchResult(int first, int last, int groupCount,
+                             int groups[], String text)
+        {
+            this.first = first;
+            this.last = last;
+            this.groupCount = groupCount;
+            this.groups = groups;
+            this.text = text;
+        }
+
+        @Override
+        public int start() {
+            return first;
+        }
+
+        @Override
+        public int start(int group) {
+            if (group < 0 || group > groupCount)
+                throw new IndexOutOfBoundsException("No group " + group);
+            return groups[group * 2];
+        }
+
+        @Override
+        public int end() {
+            return last;
+        }
+
+        @Override
+        public int end(int group) {
+            if (group < 0 || group > groupCount)
+                throw new IndexOutOfBoundsException("No group " + group);
+            return groups[group * 2 + 1];
+        }
+
+        @Override
+        public int groupCount() {
+            return groupCount;
+        }
+
+        @Override
+        public String group() {
+            return group(0);
+        }
+
+        @Override
+        public String group(int group) {
+            if (group < 0 || group > groupCount)
+                throw new IndexOutOfBoundsException("No group " + group);
+            if ((groups[group*2] == -1) || (groups[group*2+1] == -1))
+                return null;
+            return text.subSequence(groups[group * 2], groups[group * 2 + 1]).toString();
+        }
     }
 
     /**
@@ -284,6 +363,7 @@
             groups[i] = -1;
         for (int i = 0; i < locals.length; i++)
             locals[i] = -1;
+        modCount++;
         return this;
     }
 
@@ -308,6 +388,7 @@
         lastAppendPosition = 0;
         from = 0;
         to = getTextLength();
+        modCount++;
         return this;
     }
 
@@ -803,6 +884,7 @@
         // Append the match substitution
         sb.append(result);
         lastAppendPosition = last;
+        modCount++;
         return this;
     }
 
@@ -892,6 +974,7 @@
         // Append the match substitution
         sb.append(result);
         lastAppendPosition = last;
+        modCount++;
         return this;
     }
 
@@ -1078,6 +1161,183 @@
     }
 
     /**
+     * Replaces every subsequence of the input sequence that matches the
+     * pattern with the result of applying the given replacer function to the
+     * match result of this matcher corresponding to that subsequence.
+     * Exceptions thrown by the function are relayed to the caller.
+     *
+     * <p> This method first resets this matcher.  It then scans the input
+     * sequence looking for matches of the pattern.  Characters that are not
+     * part of any match are appended directly to the result string; each match
+     * is replaced in the result by the applying the replacer function that
+     * returns a replacement string.  Each replacement string may contain
+     * references to captured subsequences as in the {@link #appendReplacement
+     * appendReplacement} method.
+     *
+     * <p> Note that backslashes (<tt>\</tt>) and dollar signs (<tt>$</tt>) in
+     * a replacement string may cause the results to be different than if it
+     * were being treated as a literal replacement string. Dollar signs may be
+     * treated as references to captured subsequences as described above, and
+     * backslashes are used to escape literal characters in the replacement
+     * string.
+     *
+     * <p> Given the regular expression <tt>dog</tt>, the input
+     * <tt>"zzzdogzzzdogzzz"</tt>, and the function
+     * <tt>mr -> mr.group().toUpperCase()</tt>, an invocation of this method on
+     * a matcher for that expression would yield the string
+     * <tt>"zzzDOGzzzDOGzzz"</tt>.
+     *
+     * <p> Invoking this method changes this matcher's state.  If the matcher
+     * is to be used in further matching operations then it should first be
+     * reset.  </p>
+     *
+     * <p> The replacer function should not modify this matcher's state during
+     * replacement.  This method will, on a best-effort basis, throw a
+     * {@link java.util.ConcurrentModificationException} if such modification is
+     * detected.
+     *
+     * <p> The state of each match result passed to the replacer function is
+     * guaranteed to be constant only for the duration of the replacer function
+     * call and only if the replacer function does not modify this matcher's
+     * state.
+     *
+     * @implNote
+     * This implementation applies the replacer function to this matcher, which
+     * is an instance of {@code MatchResult}.
+     *
+     * @param  replacer
+     *         The function to be applied to the match result of this matcher
+     *         that returns a replacement string.
+     * @return  The string constructed by replacing each matching subsequence
+     *          with the result of applying the replacer function to that
+     *          matched subsequence, substituting captured subsequences as
+     *          needed.
+     * @throws NullPointerException if the replacer function is null
+     * @throws ConcurrentModificationException if it is detected, on a
+     *         best-effort basis, that the replacer function modified this
+     *         matcher's state
+     * @since 1.9
+     */
+    public String replaceAll(Function<MatchResult, String> replacer) {
+        Objects.requireNonNull(replacer);
+        reset();
+        boolean result = find();
+        if (result) {
+            StringBuilder sb = new StringBuilder();
+            do {
+                int ec = modCount;
+                String replacement =  replacer.apply(this);
+                if (ec != modCount)
+                    throw new ConcurrentModificationException();
+                appendReplacement(sb, replacement);
+                result = find();
+            } while (result);
+            appendTail(sb);
+            return sb.toString();
+        }
+        return text.toString();
+    }
+
+    /**
+     * Returns a stream of match results for each subsequence of the input
+     * sequence that matches the pattern.  The match results occur in the
+     * same order as the matching subsequences in the input sequence.
+     *
+     * <p> Each match result is produced as if by {@link #toMatchResult()}.
+     *
+     * <p> This method does not reset this matcher.  Matching starts on
+     * initiation of the terminal stream operation either at the beginning of
+     * this matcher's region, or, if the matcher has not since been reset, at
+     * the first character not matched by a previous match.
+     *
+     * <p> If the matcher is to be used for further matching operations after
+     * the terminal stream operation completes then it should be first reset.
+     *
+     * <p> This matcher's state should not be modified during execution of the
+     * returned stream's pipeline.  The returned stream's source
+     * {@code Spliterator} is <em>fail-fast</em> and will, on a best-effort
+     * basis, throw a {@link java.util.ConcurrentModificationException} if such
+     * modification is detected.
+     *
+     * @return a sequential stream of match results.
+     * @since 1.9
+     */
+    public Stream<MatchResult> results() {
+        class MatchResultIterator implements Iterator<MatchResult> {
+            // -ve for call to find, 0 for not found, 1 for found
+            int state = -1;
+            // State for concurrent modification checking
+            // -1 for uninitialized
+            int expectedCount = -1;
+            // The input sequence as a string, set once only after first find
+            // Avoids repeated conversion from CharSequence for each match
+            String textAsString;
+
+            @Override
+            public MatchResult next() {
+                if (expectedCount >= 0 && expectedCount != modCount)
+                    throw new ConcurrentModificationException();
+
+                if (!hasNext())
+                    throw new NoSuchElementException();
+
+                state = -1;
+                return toMatchResult(textAsString);
+            }
+
+            @Override
+            public boolean hasNext() {
+                if (state >= 0)
+                    return state == 1;
+
+                // Defer throwing ConcurrentModificationException to when next
+                // or forEachRemaining is called.  The is consistent with other
+                // fail-fast implementations.
+                if (expectedCount >= 0 && expectedCount != modCount)
+                    return true;
+
+                boolean found = find();
+                // Capture the input sequence as a string on first find
+                if (found && state < 0)
+                    textAsString = text.toString();
+                state = found ? 1 : 0;
+                expectedCount = modCount;
+                return found;
+            }
+
+            @Override
+            public void forEachRemaining(Consumer<? super MatchResult> action) {
+                if (expectedCount >= 0 && expectedCount != modCount)
+                    throw new ConcurrentModificationException();
+
+                int s = state;
+                if (s == 0)
+                    return;
+
+                // Set state to report no more elements on further operations
+                state = 0;
+                expectedCount = -1;
+
+                // Perform a first find if required
+                if (s < 0 && !find())
+                    return;
+
+                // Capture the input sequence as a string on first find
+                textAsString = text.toString();
+
+                do {
+                    int ec = modCount;
+                    action.accept(toMatchResult(textAsString));
+                    if (ec != modCount)
+                        throw new ConcurrentModificationException();
+                } while (find());
+            }
+        }
+        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
+                new MatchResultIterator(), Spliterator.ORDERED | Spliterator.NONNULL), false);
+    }
+
+    /**
      * Replaces the first subsequence of the input sequence that matches the
      * pattern with the given replacement string.
      *
@@ -1123,6 +1383,79 @@
     }
 
     /**
+     * Replaces the first subsequence of the input sequence that matches the
+     * pattern with the result of applying the given replacer function to the
+     * match result of this matcher corresponding to that subsequence.
+     * Exceptions thrown by the replace function are relayed to the caller.
+     *
+     * <p> This method first resets this matcher.  It then scans the input
+     * sequence looking for a match of the pattern.  Characters that are not
+     * part of the match are appended directly to the result string; the match
+     * is replaced in the result by the applying the replacer function that
+     * returns a replacement string.  The replacement string may contain
+     * references to captured subsequences as in the {@link #appendReplacement
+     * appendReplacement} method.
+     *
+     * <p>Note that backslashes (<tt>\</tt>) and dollar signs (<tt>$</tt>) in
+     * the replacement string may cause the results to be different than if it
+     * were being treated as a literal replacement string. Dollar signs may be
+     * treated as references to captured subsequences as described above, and
+     * backslashes are used to escape literal characters in the replacement
+     * string.
+     *
+     * <p> Given the regular expression <tt>dog</tt>, the input
+     * <tt>"zzzdogzzzdogzzz"</tt>, and the function
+     * <tt>mr -> mr.group().toUpperCase()</tt>, an invocation of this method on
+     * a matcher for that expression would yield the string
+     * <tt>"zzzDOGzzzdogzzz"</tt>.
+     *
+     * <p> Invoking this method changes this matcher's state.  If the matcher
+     * is to be used in further matching operations then it should first be
+     * reset.
+     *
+     * <p> The replacer function should not modify this matcher's state during
+     * replacement.  This method will, on a best-effort basis, throw a
+     * {@link java.util.ConcurrentModificationException} if such modification is
+     * detected.
+     *
+     * <p> The state of the match result passed to the replacer function is
+     * guaranteed to be constant only for the duration of the replacer function
+     * call and only if the replacer function does not modify this matcher's
+     * state.
+     *
+     * @implNote
+     * This implementation applies the replacer function to this matcher, which
+     * is an instance of {@code MatchResult}.
+     *
+     * @param  replacer
+     *         The function to be applied to the match result of this matcher
+     *         that returns a replacement string.
+     * @return  The string constructed by replacing the first matching
+     *          subsequence with the result of applying the replacer function to
+     *          the matched subsequence, substituting captured subsequences as
+     *          needed.
+     * @throws NullPointerException if the replacer function is null
+     * @throws ConcurrentModificationException if it is detected, on a
+     *         best-effort basis, that the replacer function modified this
+     *         matcher's state
+     * @since 1.9
+     */
+    public String replaceFirst(Function<MatchResult, String> replacer) {
+        Objects.requireNonNull(replacer);
+        reset();
+        if (!find())
+            return text.toString();
+        StringBuilder sb = new StringBuilder();
+        int ec = modCount;
+        String replacement = replacer.apply(this);
+        if (ec != modCount)
+            throw new ConcurrentModificationException();
+        appendReplacement(sb, replacement);
+        appendTail(sb);
+        return sb.toString();
+    }
+
+    /**
      * Sets the limits of this matcher's region. The region is the part of the
      * input sequence that will be searched to find a match. Invoking this
      * method resets the matcher, and then sets the region to start at the
@@ -1365,6 +1698,7 @@
         if (!result)
             this.first = -1;
         this.oldLast = this.last;
+        this.modCount++;
         return result;
     }
 
@@ -1387,6 +1721,7 @@
         if (!result)
             this.first = -1;
         this.oldLast = this.last;
+        this.modCount++;
         return result;
     }
 
--- a/test/java/util/regex/PatternStreamTest.java	Tue Mar 03 10:30:44 2015 +0300
+++ b/test/java/util/regex/PatternStreamTest.java	Tue Mar 03 12:30:48 2015 +0100
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2015 Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,8 +23,8 @@
 
 /**
  * @test
- * @bug 8016846 8024341
- * @summary Unit tests for wrapping classes should delegate to default methods
+ * @bug 8016846 8024341 8071479
+ * @summary Unit tests stream and lambda-based methods on Pattern and Matcher
  * @library ../stream/bootlib
  * @build java.util.stream.OpTestCase
  * @run testng/othervm PatternStreamTest
@@ -34,158 +34,283 @@
 import org.testng.annotations.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.ConcurrentModificationException;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Supplier;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.LambdaTestHelpers;
 import java.util.stream.OpTestCase;
 import java.util.stream.Stream;
 import java.util.stream.TestData;
 
+import static org.testng.Assert.*;
+
 @Test
 public class PatternStreamTest extends OpTestCase {
 
-    @DataProvider(name = "Stream<String>")
+    @DataProvider(name = "Patterns")
     public static Object[][] makeStreamTestData() {
+        // Each item must match the type signature of the consumer of this data
+        // String, String, Pattern
         List<Object[]> data = new ArrayList<>();
 
-        String description = "";
-        String input = "awgqwefg1fefw4vssv1vvv1";
-        Pattern pattern = Pattern.compile("4");
-        List<String> expected = new ArrayList<>();
-        expected.add("awgqwefg1fefw");
-        expected.add("vssv1vvv1");
+        String description = "All matches";
+        String input = "XXXXXX";
+        Pattern pattern = Pattern.compile("X");
+        data.add(new Object[]{description, input, pattern});
 
-        // Must match the type signature of the consumer of this data, testStrings
-        // String, String, Pattern, List<String>
-        data.add(new Object[]{description, input, pattern, expected});
+        description = "Bounded every other match";
+        input = "XYXYXYYXYX";
+        pattern = Pattern.compile("X");
+        data.add(new Object[]{description, input, pattern});
+
+        description = "Every other match";
+        input = "YXYXYXYYXYXY";
+        pattern = Pattern.compile("X");
+        data.add(new Object[]{description, input, pattern});
+
+        description = "";
+        input = "awgqwefg1fefw4vssv1vvv1";
+        pattern = Pattern.compile("4");
+        data.add(new Object[]{description, input, pattern});
 
         input = "afbfq\u00a3abgwgb\u00a3awngnwggw\u00a3a\u00a3ahjrnhneerh";
         pattern = Pattern.compile("\u00a3a");
-        expected = new ArrayList<>();
-        expected.add("afbfq");
-        expected.add("bgwgb");
-        expected.add("wngnwggw");
-        expected.add("");
-        expected.add("hjrnhneerh");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         input = "awgqwefg1fefw4vssv1vvv1";
         pattern = Pattern.compile("1");
-        expected = new ArrayList<>();
-        expected.add("awgqwefg");
-        expected.add("fefw4vssv");
-        expected.add("vvv");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         input = "a\u4ebafg1fefw\u4eba4\u9f9cvssv\u9f9c1v\u672c\u672cvv";
         pattern = Pattern.compile("1");
-        expected = new ArrayList<>();
-        expected.add("a\u4ebafg");
-        expected.add("fefw\u4eba4\u9f9cvssv\u9f9c");
-        expected.add("v\u672c\u672cvv");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         input = "1\u56da23\u56da456\u56da7890";
         pattern = Pattern.compile("\u56da");
-        expected = new ArrayList<>();
-        expected.add("1");
-        expected.add("23");
-        expected.add("456");
-        expected.add("7890");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         input = "1\u56da23\u9f9c\u672c\u672c\u56da456\u56da\u9f9c\u672c7890";
         pattern = Pattern.compile("\u56da");
-        expected = new ArrayList<>();
-        expected.add("1");
-        expected.add("23\u9f9c\u672c\u672c");
-        expected.add("456");
-        expected.add("\u9f9c\u672c7890");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         description = "Empty input";
         input = "";
         pattern = Pattern.compile("\u56da");
-        expected = new ArrayList<>();
-        expected.add("");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         description = "Empty input with empty pattern";
         input = "";
         pattern = Pattern.compile("");
-        expected = new ArrayList<>();
-        expected.add("");
-
-        data.add(new Object[]{description, input, pattern, expected});
-
+        data.add(new Object[]{description, input, pattern});
 
         description = "Multiple separators";
         input = "This is,testing: with\tdifferent separators.";
         pattern = Pattern.compile("[ \t,:.]");
-        expected = new ArrayList<>();
-        expected.add("This");
-        expected.add("is");
-        expected.add("testing");
-        expected.add("");
-        expected.add("with");
-        expected.add("different");
-        expected.add("separators");
-
+        data.add(new Object[]{description, input, pattern});
 
         description = "Repeated separators within and at end";
         input = "boo:and:foo";
         pattern = Pattern.compile("o");
-        expected = new ArrayList<>();
-        expected.add("b");
-        expected.add("");
-        expected.add(":and:f");
-
+        data.add(new Object[]{description, input, pattern});
 
         description = "Many repeated separators within and at end";
         input = "booooo:and:fooooo";
         pattern = Pattern.compile("o");
-        expected = new ArrayList<>();
-        expected.add("b");
-        expected.add("");
-        expected.add("");
-        expected.add("");
-        expected.add("");
-        expected.add(":and:f");
+        data.add(new Object[]{description, input, pattern});
 
         description = "Many repeated separators before last match";
         input = "fooooo:";
         pattern = Pattern.compile("o");
-        expected = new ArrayList<>();
-        expected.add("f");
-        expected.add("");
-        expected.add("");
-        expected.add("");
-        expected.add("");
-        expected.add(":");
+        data.add(new Object[] {description, input, pattern});
 
-        data.add(new Object[] {description, input, pattern, expected});
         return data.toArray(new Object[0][]);
     }
 
-    @Test(dataProvider = "Stream<String>")
-    public void testStrings(String description, String input, Pattern pattern, List<String> expected) {
+    @Test(dataProvider = "Patterns")
+    public void testPatternSplitAsStream(String description, String input, Pattern pattern) {
+        // Derive expected result from pattern.split
+        List<String> expected = Arrays.asList(pattern.split(input));
+
         Supplier<Stream<String>> ss =  () -> pattern.splitAsStream(input);
         withData(TestData.Factory.ofSupplier(description, ss))
                 .stream(LambdaTestHelpers.identity())
                 .expectedResult(expected)
                 .exercise();
     }
+
+    @Test(dataProvider = "Patterns")
+    public void testReplaceFirst(String description, String input, Pattern pattern) {
+        // Derive expected result from Matcher.replaceFirst(String )
+        String expected = pattern.matcher(input).replaceFirst("R");
+        String actual = pattern.matcher(input).replaceFirst(r -> "R");
+        assertEquals(actual, expected);
+    }
+
+    @Test(dataProvider = "Patterns")
+    public void testReplaceAll(String description, String input, Pattern pattern) {
+        // Derive expected result from Matcher.replaceAll(String )
+        String expected = pattern.matcher(input).replaceAll("R");
+        String actual = pattern.matcher(input).replaceAll(r -> "R");
+        assertEquals(actual, expected);
+
+        // Derive expected result from Matcher.find
+        Matcher m = pattern.matcher(input);
+        int expectedMatches = 0;
+        while (m.find()) {
+            expectedMatches++;
+        }
+        AtomicInteger actualMatches = new AtomicInteger();
+        pattern.matcher(input).replaceAll(r -> "R" + actualMatches.incrementAndGet());
+        assertEquals(expectedMatches, actualMatches.get());
+    }
+
+    @Test(dataProvider = "Patterns")
+    public void testMatchResults(String description, String input, Pattern pattern) {
+        // Derive expected result from Matcher.find
+        Matcher m = pattern.matcher(input);
+        List<MatchResultHolder> expected = new ArrayList<>();
+        while (m.find()) {
+            expected.add(new MatchResultHolder(m));
+        }
+
+        Supplier<Stream<MatchResult>> ss =  () -> pattern.matcher(input).results();
+        withData(TestData.Factory.ofSupplier(description, ss))
+                .stream(s -> s.map(MatchResultHolder::new))
+                .expectedResult(expected)
+                .exercise();
+    }
+
+    public void testFailfastMatchResults() {
+        Pattern p = Pattern.compile("X");
+        Matcher m = p.matcher("XX");
+
+        Stream<MatchResult> s = m.results();
+        m.find();
+        // Should start on the second match
+        assertEquals(s.count(), 1);
+
+        // Fail fast without short-circuit
+        // Exercises Iterator.forEachRemaining
+        m.reset();
+        try {
+            m.results().peek(mr -> m.reset()).count();
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+
+        m.reset();
+        try {
+            m.results().peek(mr -> m.find()).count();
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+
+        // Fail fast with short-circuit
+        // Exercises Iterator.hasNext/next
+        m.reset();
+        try {
+            m.results().peek(mr -> m.reset()).limit(2).count();
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+
+        m.reset();
+        try {
+            m.results().peek(mr -> m.find()).limit(2).count();
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+    }
+
+    public void testFailfastReplace() {
+        Pattern p = Pattern.compile("X");
+        Matcher m = p.matcher("XX");
+
+        // Fail fast without short-circuit
+        // Exercises Iterator.forEachRemaining
+        m.reset();
+        try {
+            m.replaceFirst(mr -> { m.reset(); return "Y"; });
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+
+        m.reset();
+        try {
+            m.replaceAll(mr -> { m.reset(); return "Y"; });
+            fail();
+        } catch (ConcurrentModificationException e) {
+            // Should reach here
+        }
+    }
+
+    // A holder of MatchResult that can compare
+    static class MatchResultHolder implements Comparable<MatchResultHolder> {
+        final MatchResult mr;
+
+        MatchResultHolder(Matcher m) {
+            this(m.toMatchResult());
+        }
+
+        MatchResultHolder(MatchResult mr) {
+            this.mr = mr;
+        }
+
+        @Override
+        public int compareTo(MatchResultHolder that) {
+            int c = that.mr.group().compareTo(this.mr.group());
+            if (c != 0)
+                return c;
+
+            c = Integer.compare(that.mr.start(), this.mr.start());
+            if (c != 0)
+                return c;
+
+            c = Integer.compare(that.mr.end(), this.mr.end());
+            if (c != 0)
+                return c;
+
+            c = Integer.compare(that.mr.groupCount(), this.mr.groupCount());
+            if (c != 0)
+                return c;
+
+            for (int g = 0; g < this.mr.groupCount(); g++) {
+                c = that.mr.group(g).compareTo(this.mr.group(g));
+                if (c != 0)
+                    return c;
+
+                c = Integer.compare(that.mr.start(g), this.mr.start(g));
+                if (c != 0)
+                    return c;
+
+                c = Integer.compare(that.mr.end(g), this.mr.end(g));
+                if (c != 0)
+                    return c;
+            }
+            return 0;
+        }
+
+        @Override
+        public boolean equals(Object that) {
+            if (this == that) return true;
+            if (that == null || getClass() != that.getClass()) return false;
+
+            return this.compareTo((MatchResultHolder) that) == 0;
+        }
+
+        @Override
+        public int hashCode() {
+            return mr.group().hashCode();
+        }
+    }
 }
--- a/test/java/util/regex/RegExTest.java	Tue Mar 03 10:30:44 2015 +0300
+++ b/test/java/util/regex/RegExTest.java	Tue Mar 03 12:30:48 2015 +0100
@@ -35,6 +35,7 @@
  * 8027645 8035076 8039124 8035975
  */
 
+import java.util.function.Function;
 import java.util.regex.*;
 import java.util.Random;
 import java.io.*;
@@ -291,24 +292,26 @@
     }
 
     private static void nullArgumentTest() {
-        check(new Runnable() { public void run() { Pattern.compile(null); }});
-        check(new Runnable() { public void run() { Pattern.matches(null, null); }});
-        check(new Runnable() { public void run() { Pattern.matches("xyz", null);}});
-        check(new Runnable() { public void run() { Pattern.quote(null);}});
-        check(new Runnable() { public void run() { Pattern.compile("xyz").split(null);}});
-        check(new Runnable() { public void run() { Pattern.compile("xyz").matcher(null);}});
+        check(() -> Pattern.compile(null));
+        check(() -> Pattern.matches(null, null));
+        check(() -> Pattern.matches("xyz", null));
+        check(() -> Pattern.quote(null));
+        check(() -> Pattern.compile("xyz").split(null));
+        check(() -> Pattern.compile("xyz").matcher(null));
 
         final Matcher m = Pattern.compile("xyz").matcher("xyz");
         m.matches();
-        check(new Runnable() { public void run() { m.appendTail((StringBuffer)null);}});
-        check(new Runnable() { public void run() { m.appendTail((StringBuilder)null);}});
-        check(new Runnable() { public void run() { m.replaceAll(null);}});
-        check(new Runnable() { public void run() { m.replaceFirst(null);}});
-        check(new Runnable() { public void run() { m.appendReplacement((StringBuffer)null, null);}});
-        check(new Runnable() { public void run() { m.appendReplacement((StringBuilder)null, null);}});
-        check(new Runnable() { public void run() { m.reset(null);}});
-        check(new Runnable() { public void run() { Matcher.quoteReplacement(null);}});
-        //check(new Runnable() { public void run() { m.usePattern(null);}});
+        check(() -> m.appendTail((StringBuffer) null));
+        check(() -> m.appendTail((StringBuilder)null));
+        check(() -> m.replaceAll((String) null));
+        check(() -> m.replaceAll((Function<MatchResult, String>)null));
+        check(() -> m.replaceFirst((String)null));
+        check(() -> m.replaceFirst((Function<MatchResult, String>) null));
+        check(() -> m.appendReplacement((StringBuffer)null, null));
+        check(() -> m.appendReplacement((StringBuilder)null, null));
+        check(() -> m.reset(null));
+        check(() -> Matcher.quoteReplacement(null));
+        //check(() -> m.usePattern(null));
 
         report("Null Argument");
     }