Variation of PR #227 from Rob Spoor (robtimus) but does not add a new
class.

- Add AbstractCharacterFilterReader(Reader, IntPredicate).
- Add CharacterFilterReader(Reader, IntPredicate).
- Add AbstractCharacterFilterReader(Reader, IntPredicate).
- Add CharacterFilterReaderIntPredicateTest.
diff --git a/src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java b/src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java
index 70f92a2..02147ca 100644
--- a/src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java
+++ b/src/main/java/org/apache/commons/io/input/AbstractCharacterFilterReader.java
@@ -21,19 +21,46 @@
 import java.io.FilterReader;
 import java.io.IOException;
 import java.io.Reader;
+import java.util.function.IntPredicate;
 
 /**
  * A filter reader that filters out characters where subclasses decide which characters to filter out.
  */
 public abstract class AbstractCharacterFilterReader extends FilterReader {
 
+    private static final IntPredicate SKIP_NONE = ch -> false;
+
+    private final IntPredicate skip;
+
     /**
      * Constructs a new reader.
      *
      * @param reader the reader to filter
      */
     protected AbstractCharacterFilterReader(final Reader reader) {
+        this(reader, SKIP_NONE);
+    }
+
+    /**
+     * Constructs a new reader.
+     *
+     * @param reader the reader to filter.
+     * @param skip Skip test.
+     * @since 2.9.0
+     */
+    protected AbstractCharacterFilterReader(final Reader reader, final IntPredicate skip) {
         super(reader);
+        this.skip = skip == null ? SKIP_NONE : skip;
+    }
+
+    /**
+     * Returns true if the given character should be filtered out, false to keep the character.
+     *
+     * @param ch the character to test.
+     * @return true if the given character should be filtered out, false to keep the character.
+     */
+    protected boolean filter(final int ch) {
+        return skip.test(ch);
     }
 
     @Override
@@ -45,14 +72,6 @@
         return ch;
     }
 
-    /**
-     * Returns true if the given character should be filtered out, false to keep the character.
-     *
-     * @param ch the character to test.
-     * @return true if the given character should be filtered out, false to keep the character.
-     */
-    protected abstract boolean filter(int ch);
-
     @Override
     public int read(final char[] cbuf, final int off, final int len) throws IOException {
         final int read = super.read(cbuf, off, len);
diff --git a/src/main/java/org/apache/commons/io/input/CharacterFilterReader.java b/src/main/java/org/apache/commons/io/input/CharacterFilterReader.java
index ff949bd..c6edd5b 100644
--- a/src/main/java/org/apache/commons/io/input/CharacterFilterReader.java
+++ b/src/main/java/org/apache/commons/io/input/CharacterFilterReader.java
@@ -17,6 +17,7 @@
 package org.apache.commons.io.input;
 
 import java.io.Reader;
+import java.util.function.IntPredicate;
 
 /**
  * A filter reader that filters out a given character represented as an {@code int} code point, handy to remove
@@ -25,8 +26,6 @@
  */
 public class CharacterFilterReader extends AbstractCharacterFilterReader {
 
-    private final int skip;
-
     /**
      * Constructs a new reader.
      *
@@ -36,13 +35,18 @@
      *            the character to filter out.
      */
     public CharacterFilterReader(final Reader reader, final int skip) {
-        super(reader);
-        this.skip = skip;
+        super(reader, c -> c == skip);
     }
 
-    @Override
-    protected boolean filter(final int ch) {
-        return ch == skip;
+    /**
+     * Constructs a new reader.
+     *
+     * @param reader the reader to filter.
+     * @param skip Skip test.
+     * @since 2.9.0
+     */
+    public CharacterFilterReader(final Reader reader, final IntPredicate skip) {
+        super(reader, skip);
     }
 
 }
diff --git a/src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java b/src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java
index 064cab6..a7577a9 100644
--- a/src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java
+++ b/src/main/java/org/apache/commons/io/input/CharacterSetFilterReader.java
@@ -32,9 +32,6 @@
  */
 public class CharacterSetFilterReader extends AbstractCharacterFilterReader {
 
-    private static final Set<Integer> EMPTY_SET = Collections.emptySet();
-    private final Set<Integer> skipSet;
-
     /**
      * Constructs a new reader.
      *
@@ -53,14 +50,7 @@
      * @param skip the set of characters to filter out.
      */
     public CharacterSetFilterReader(final Reader reader, final Set<Integer> skip) {
-        super(reader);
-        this.skipSet = skip == null ? EMPTY_SET : Collections.unmodifiableSet(skip);
-    }
-
-    @Override
-    protected boolean filter(final int ch) {
-        // Note WRT Integer.valueOf(): You can increase the Integer cache with a system property, see {@link Integer}.
-        return skipSet.contains(Integer.valueOf(ch));
+        super(reader, c -> skip == null ? null : Collections.unmodifiableSet(skip).contains(Integer.valueOf(c)));
     }
 
 }
diff --git a/src/test/java/org/apache/commons/io/input/CharacterFilterReaderIntPredicateTest.java b/src/test/java/org/apache/commons/io/input/CharacterFilterReaderIntPredicateTest.java
new file mode 100644
index 0000000..fba9c4a
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/input/CharacterFilterReaderIntPredicateTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io.input;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.function.IntPredicate;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.StringBuilderWriter;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link CharacterFilterReader} with an {@link IntPredicate}.
+ */
+public class CharacterFilterReaderIntPredicateTest {
+
+    @Test
+    public void testInputSize0FilterAll() throws IOException {
+        final StringReader input = new StringReader(StringUtils.EMPTY);
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, ch -> true)) {
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testInputSize1FilterAll() throws IOException {
+        try (StringReader input = new StringReader("a");
+                CharacterFilterReader reader = new CharacterFilterReader(input, ch -> true)) {
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testInputSize2FilterAll() throws IOException {
+        final StringReader input = new StringReader("aa");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, ch -> true)) {
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testInputSize2FilterFirst() throws IOException {
+        final StringReader input = new StringReader("ab");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, ch -> ch == 'a')) {
+            assertEquals('b', reader.read());
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testInputSize2FilterLast() throws IOException {
+        final StringReader input = new StringReader("ab");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, ch -> ch == 'b')) {
+            assertEquals('a', reader.read());
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testInputSize5FilterWhitespace() throws IOException {
+        final StringReader input = new StringReader(" a b ");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, Character::isWhitespace)) {
+            assertEquals('a', reader.read());
+            assertEquals('b', reader.read());
+            assertEquals(-1, reader.read());
+        }
+    }
+
+    @Test
+    public void testReadIntoBuffer() throws IOException {
+        final StringReader input = new StringReader("ababcabcd");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, ch -> ch == 'b')) {
+            final char[] buff = new char[9];
+            final int charCount = reader.read(buff);
+            assertEquals(6, charCount);
+            assertEquals("aacacd", new String(buff, 0, charCount));
+        }
+    }
+
+    @Test
+    public void testReadIntoBufferFilterWhitespace() throws IOException {
+        final StringReader input = new StringReader(" a b a b c a b c d ");
+        try (CharacterFilterReader reader = new CharacterFilterReader(input, Character::isWhitespace)) {
+            final char[] buff = new char[19];
+            final int charCount = reader.read(buff);
+            assertEquals(9, charCount);
+            assertEquals("ababcabcd", new String(buff, 0, charCount));
+        }
+    }
+
+    @Test
+    public void testReadUsingReader() throws IOException {
+        final StringReader input = new StringReader("ababcabcd");
+        try (StringBuilderWriter output = new StringBuilderWriter();
+                CharacterFilterReader reader = new CharacterFilterReader(input, ch -> ch == 'b')) {
+            IOUtils.copy(reader, output);
+            assertEquals("aacacd", output.toString());
+        }
+    }
+
+    @Test
+    public void testReadUsingReaderFilterWhitespace() throws IOException {
+        final StringReader input = new StringReader(" a b a b c a b c d ");
+        try (StringBuilderWriter output = new StringBuilderWriter();
+                CharacterFilterReader reader = new CharacterFilterReader(input, Character::isWhitespace)) {
+            IOUtils.copy(reader, output);
+            assertEquals("ababcabcd", output.toString());
+        }
+    }
+
+}
\ No newline at end of file