Snap for 11051137 from 0c4d7b72e49a04598d65c566f44504b95342d75a to 24Q1-release

Change-Id: I714f4fd5addf48715cac4ccd95aee0668d6a2507
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1420421..120d34f 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -45,7 +45,7 @@
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+      uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         persist-credentials: false
     - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
@@ -57,7 +57,7 @@
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
+      uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
       with:
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -68,7 +68,7 @@
     # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
     # If this step fails, then you should remove it and run the build manually (see below)
     - name: Autobuild
-      uses: github/codeql-action/autobuild@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
+      uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
 
     # ℹī¸ Command-line programs to run using the OS shell.
     # 📚 https://git.io/JvXDl
@@ -82,4 +82,4 @@
     #   make release
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
+      uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index f100bf3..5c778d5 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -29,7 +29,7 @@
         java: [ 8 ]
 
     steps:
-    - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         persist-credentials: false
     - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
@@ -44,7 +44,7 @@
         distribution: 'temurin'
         java-version: ${{ matrix.java }}
     - name: Build with Maven
-      run: mvn -V test jacoco:report --file pom.xml --no-transfer-progress
+      run: mvn --show-version --batch-mode --no-transfer-progress test jacoco:report
 
     - name: Upload coverage to Codecov
       uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 6bd303e..3ed497d 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -28,22 +28,22 @@
     strategy:
       matrix:
         os: [ubuntu-latest, windows-latest, macos-latest]
-        java: [ 8, 11, 17 ]
+        java: [ 8, 11, 17, 21 ]
         experimental: [false]
 #        include:
-#          - java: 18-ea
+#          - java: 22-ea
 #            os: ubuntu-latest
 #            experimental: true        
-#          - java: 18-ea
+#          - java: 22-ea
 #            os: windows-latest
 #            experimental: true        
-#          - java: 18-ea
+#          - java: 22-ea
 #            os: macos-latest
 #            experimental: true        
       fail-fast: false
         
     steps:
-    - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+    - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
       with:
         persist-credentials: false
     - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2
@@ -58,4 +58,4 @@
         distribution: 'temurin'
         java-version: ${{ matrix.java }}
     - name: Build with Maven
-      run: mvn -V --file pom.xml --no-transfer-progress -DtrimStackTrace=false
+      run: mvn --show-version --batch-mode --no-transfer-progress -DtrimStackTrace=false
diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml
index 3f533c0..890f049 100644
--- a/.github/workflows/scorecards-analysis.yml
+++ b/.github/workflows/scorecards-analysis.yml
@@ -40,12 +40,12 @@
     steps:
 
       - name: "Checkout code"
-        uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
         with:
           persist-credentials: false
 
       - name: "Run analysis"
-        uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031    # 2.2.0
+        uses: ossf/scorecard-action@483ef80eb98fb506c348f7d62e28055e49fe2398    # 2.3.0
         with:
           results_file: results.sarif
           results_format: sarif
@@ -64,6 +64,6 @@
           retention-days: 5
 
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@04daf014b50eaf774287bf3f0f1869d4b4c4b913    # 2.21.7
+        uses: github/codeql-action/upload-sarif@0116bc2df50751f9724a2e35ef1f24d22f90e4e1    # 2.22.3
         with:
           sarif_file: results.sarif
diff --git a/METADATA b/METADATA
index b462024..3833c5d 100644
--- a/METADATA
+++ b/METADATA
@@ -13,11 +13,11 @@
     type: GIT
     value: "https://github.com/apache/commons-io.git"
   }
-  version: "rel/commons-io-2.14.0"
+  version: "rel/commons-io-2.15.0"
   license_type: NOTICE
   last_upgrade_date {
     year: 2023
-    month: 10
-    day: 11
+    month: 11
+    day: 1
   }
 }
diff --git a/README.md b/README.md
index 25c792e..48bf012 100644
--- a/README.md
+++ b/README.md
@@ -46,7 +46,7 @@
 [![GitHub Actions Status](https://github.com/apache/commons-io/workflows/Java%20CI/badge.svg)](https://github.com/apache/commons-io/actions)
 [![Coverage Status](https://codecov.io/gh/apache/commons-io/branch/master/graph/badge.svg)](https://app.codecov.io/gh/apache/commons-io)
 [![Maven Central](https://maven-badges.herokuapp.com/maven-central/commons-io/commons-io/badge.svg?gav=true)](https://maven-badges.herokuapp.com/maven-central/commons-io/commons-io/?gav=true)
-[![Javadocs](https://javadoc.io/badge/commons-io/commons-io/2.14.0.svg)](https://javadoc.io/doc/commons-io/commons-io/2.14.0)
+[![Javadocs](https://javadoc.io/badge/commons-io/commons-io/2.15.0.svg)](https://javadoc.io/doc/commons-io/commons-io/2.15.0)
 [![CodeQL](https://github.com/apache/commons-io/workflows/CodeQL/badge.svg)](https://github.com/apache/commons-io/actions/workflows/codeql-analysis.yml?query=workflow%3ACodeQL)
 [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/apache/commons-io/badge)](https://api.securityscorecards.dev/projects/github.com/apache/commons-io)
 
@@ -60,8 +60,8 @@
 The [Javadoc](https://commons.apache.org/proper/commons-io/apidocs) can be browsed.
 Questions related to the usage of Apache Commons IO should be posted to the [user mailing list][ml].
 
-Where can I get the latest release?
------------------------------------
+Getting the latest release
+--------------------------
 You can download source and binaries from our [download page](https://commons.apache.org/proper/commons-io/download_io.cgi).
 
 Alternatively, you can pull it from  the central Maven repositories:
@@ -70,17 +70,25 @@
 <dependency>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
-  <version>2.14.0</version>
+  <version>2.15.0</version>
 </dependency>
 ```
 
+Building
+--------
+
+Building requires a Java JDK and [Apache Maven](https://maven.apache.org/). 
+The required Java version is found in the `pom.xml` as the `maven.compiler.source` property.
+
+From a command shell, run `mvn` without arguments to invoke the default Maven goal to run all tests and checks.
+
 Contributing
 ------------
 
 We accept Pull Requests via GitHub. The [developer mailing list](https://commons.apache.org/mail-lists.html) is the main channel of communication for contributors.
 There are some guidelines which will make applying PRs easier for us:
 + No tabs! Please use spaces for indentation.
-+ Respect the code style.
++ Respect the existing code style for each file.
 + Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change.
 + Provide JUnit tests for your changes and make sure your changes don't break any existing tests by running ```mvn```.
 
@@ -89,13 +97,13 @@
 
 License
 -------
-This code is under the [Apache License v2](https://www.apache.org/licenses/LICENSE-2.0).
+This code is licensed under the [Apache License v2](https://www.apache.org/licenses/LICENSE-2.0).
 
 See the `NOTICE.txt` file for required notices and attributions.
 
-Donations
----------
-You like Apache Commons IO? Then [donate back to the ASF](https://www.apache.org/foundation/contributing.html) to support the development.
+Donating
+--------
+You like Apache Commons IO? Then [donate back to the ASF](https://www.apache.org/foundation/contributing.html) to support development.
 
 Additional Resources
 --------------------
@@ -109,54 +117,4 @@
 Apache Commons Components
 -------------------------
 
-| Component | GitHub Repository | Apache Homepage |
-| --------- | ----------------- | ----------------|
-| Apache Commons BCEL | [commons-bcel](https://github.com/apache/commons-bcel) | [commons-bcel](https://commons.apache.org/proper/commons-bcel) |
-| Apache Commons Beanutils | [commons-beanutils](https://github.com/apache/commons-beanutils) | [commons-beanutils](https://commons.apache.org/proper/commons-beanutils) |
-| Apache Commons BSF | [commons-bsf](https://github.com/apache/commons-bsf) | [commons-bsf](https://commons.apache.org/proper/commons-bsf) |
-| Apache Commons Build-plugin | [commons-build-plugin](https://github.com/apache/commons-build-plugin) | [commons-build-plugin](https://commons.apache.org/proper/commons-build-plugin) |
-| Apache Commons Chain | [commons-chain](https://github.com/apache/commons-chain) | [commons-chain](https://commons.apache.org/proper/commons-chain) |
-| Apache Commons CLI | [commons-cli](https://github.com/apache/commons-cli) | [commons-cli](https://commons.apache.org/proper/commons-cli) |
-| Apache Commons Codec | [commons-codec](https://github.com/apache/commons-codec) | [commons-codec](https://commons.apache.org/proper/commons-codec) |
-| Apache Commons Collections | [commons-collections](https://github.com/apache/commons-collections) | [commons-collections](https://commons.apache.org/proper/commons-collections) |
-| Apache Commons Compress | [commons-compress](https://github.com/apache/commons-compress) | [commons-compress](https://commons.apache.org/proper/commons-compress) |
-| Apache Commons Configuration | [commons-configuration](https://github.com/apache/commons-configuration) | [commons-configuration](https://commons.apache.org/proper/commons-configuration) |
-| Apache Commons Crypto | [commons-crypto](https://github.com/apache/commons-crypto) | [commons-crypto](https://commons.apache.org/proper/commons-crypto) |
-| Apache Commons CSV | [commons-csv](https://github.com/apache/commons-csv) | [commons-csv](https://commons.apache.org/proper/commons-csv) |
-| Apache Commons Daemon | [commons-daemon](https://github.com/apache/commons-daemon) | [commons-daemon](https://commons.apache.org/proper/commons-daemon) |
-| Apache Commons DBCP | [commons-dbcp](https://github.com/apache/commons-dbcp) | [commons-dbcp](https://commons.apache.org/proper/commons-dbcp) |
-| Apache Commons Dbutils | [commons-dbutils](https://github.com/apache/commons-dbutils) | [commons-dbutils](https://commons.apache.org/proper/commons-dbutils) |
-| Apache Commons Digester | [commons-digester](https://github.com/apache/commons-digester) | [commons-digester](https://commons.apache.org/proper/commons-digester) |
-| Apache Commons Email | [commons-email](https://github.com/apache/commons-email) | [commons-email](https://commons.apache.org/proper/commons-email) |
-| Apache Commons Exec | [commons-exec](https://github.com/apache/commons-exec) | [commons-exec](https://commons.apache.org/proper/commons-exec) |
-| Apache Commons Fileupload | [commons-fileupload](https://github.com/apache/commons-fileupload) | [commons-fileupload](https://commons.apache.org/proper/commons-fileupload) |
-| Apache Commons Functor | [commons-functor](https://github.com/apache/commons-functor) | [commons-functor](https://commons.apache.org/proper/commons-functor) |
-| Apache Commons Geometry | [commons-geometry](https://github.com/apache/commons-geometry) | [commons-geometry](https://commons.apache.org/proper/commons-geometry) |
-| Apache Commons Graph | [commons-graph](https://github.com/apache/commons-graph) | [commons-graph](https://commons.apache.org/proper/commons-graph) |
-| Apache Commons Imaging | [commons-imaging](https://github.com/apache/commons-imaging) | [commons-imaging](https://commons.apache.org/proper/commons-imaging) |
-| Apache Commons IO | [commons-io](https://github.com/apache/commons-io) | [commons-io](https://commons.apache.org/proper/commons-io) |
-| Apache Commons JCI | [commons-jci](https://github.com/apache/commons-jci) | [commons-jci](https://commons.apache.org/proper/commons-jci) |
-| Apache Commons JCS | [commons-jcs](https://github.com/apache/commons-jcs) | [commons-jcs](https://commons.apache.org/proper/commons-jcs) |
-| Apache Commons Jelly | [commons-jelly](https://github.com/apache/commons-jelly) | [commons-jelly](https://commons.apache.org/proper/commons-jelly) |
-| Apache Commons Jexl | [commons-jexl](https://github.com/apache/commons-jexl) | [commons-jexl](https://commons.apache.org/proper/commons-jexl) |
-| Apache Commons Jxpath | [commons-jxpath](https://github.com/apache/commons-jxpath) | [commons-jxpath](https://commons.apache.org/proper/commons-jxpath) |
-| Apache Commons Lang | [commons-lang](https://github.com/apache/commons-lang) | [commons-lang](https://commons.apache.org/proper/commons-lang) |
-| Apache Commons Logging | [commons-logging](https://github.com/apache/commons-logging) | [commons-logging](https://commons.apache.org/proper/commons-logging) |
-| Apache Commons Math | [commons-math](https://github.com/apache/commons-math) | [commons-math](https://commons.apache.org/proper/commons-math) |
-| Apache Commons Net | [commons-net](https://github.com/apache/commons-net) | [commons-net](https://commons.apache.org/proper/commons-net) |
-| Apache Commons Numbers | [commons-numbers](https://github.com/apache/commons-numbers) | [commons-numbers](https://commons.apache.org/proper/commons-numbers) |
-| Apache Commons Parent | [commons-parent](https://github.com/apache/commons-parent) | [commons-parent](https://commons.apache.org/proper/commons-parent) |
-| Apache Commons Pool | [commons-pool](https://github.com/apache/commons-pool) | [commons-pool](https://commons.apache.org/proper/commons-pool) |
-| Apache Commons Proxy | [commons-proxy](https://github.com/apache/commons-proxy) | [commons-proxy](https://commons.apache.org/proper/commons-proxy) |
-| Apache Commons RDF | [commons-rdf](https://github.com/apache/commons-rdf) | [commons-rdf](https://commons.apache.org/proper/commons-rdf) |
-| Apache Commons Release-plugin | [commons-release-plugin](https://github.com/apache/commons-release-plugin) | [commons-release-plugin](https://commons.apache.org/proper/commons-release-plugin) |
-| Apache Commons Rng | [commons-rng](https://github.com/apache/commons-rng) | [commons-rng](https://commons.apache.org/proper/commons-rng) |
-| Apache Commons Scxml | [commons-scxml](https://github.com/apache/commons-scxml) | [commons-scxml](https://commons.apache.org/proper/commons-scxml) |
-| Apache Commons Signing | [commons-signing](https://github.com/apache/commons-signing) | [commons-signing](https://commons.apache.org/proper/commons-signing) |
-| Apache Commons Skin | [commons-skin](https://github.com/apache/commons-skin) | [commons-skin](https://commons.apache.org/proper/commons-skin) |
-| Apache Commons Statistics | [commons-statistics](https://github.com/apache/commons-statistics) | [commons-statistics](https://commons.apache.org/proper/commons-statistics) |
-| Apache Commons Testing | [commons-testing](https://github.com/apache/commons-testing) | [commons-testing](https://commons.apache.org/proper/commons-testing) |
-| Apache Commons Text | [commons-text](https://github.com/apache/commons-text) | [commons-text](https://commons.apache.org/proper/commons-text) |
-| Apache Commons Validator | [commons-validator](https://github.com/apache/commons-validator) | [commons-validator](https://commons.apache.org/proper/commons-validator) |
-| Apache Commons VFS | [commons-vfs](https://github.com/apache/commons-vfs) | [commons-vfs](https://commons.apache.org/proper/commons-vfs) |
-| Apache Commons Weaver | [commons-weaver](https://github.com/apache/commons-weaver) | [commons-weaver](https://commons.apache.org/proper/commons-weaver) |
+Please see the [list of components](https://commons.apache.org/components.html)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 8300ad4..7eb5296 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -1,3 +1,82 @@
+Apache Commons IO 
+Version 2.15.0
+Release Notes
+
+INTRODUCTION:
+
+Commons IO is a package of Java utility classes like java.io.  
+Classes in this package are considered to be so standard and of such high 
+reuse as to justify existence in java.io.
+
+The Apache Commons IO library contains utility classes, stream implementations, file filters,
+file comparators, endian transformation classes, and much more.
+
+Java 8 is required.
+
+Changes in this version include:
+
+New features:
+o           Add org.apache.commons.io.channels.FileChannels. Thanks to Gary Gregory. 
+o           Add RandomAccessFiles#contentEquals(RandomAccessFile, RandomAccessFile). Thanks to Gary Gregory. 
+o           Add RandomAccessFiles#reset(RandomAccessFile). Thanks to Gary Gregory. 
+o           Add PathUtilsContentEqualsBenchmark. Thanks to Gary Gregory. 
+o           Add org.apache.commons.io.StreamIterator. Thanks to Gary Gregory. 
+o           Add MessageDigestInputStream and deprecate MessageDigestCalculatingInputStream. Thanks to Gary Gregory. 
+
+Fixed Bugs:
+o IO-810:  XmlStreamReader encoding match RE is too strict. Thanks to Laurence Gonsalves. 
+o IO-810:  Javadoc in FileUtils does not reflect code for thrown exceptions. Thanks to Gregor Dschung, Gary Gregory. 
+o IO-812:  Javadoc should mention closing Streams based on file resources. Thanks to Adam Rauch, Gary Gregory. 
+o IO-811:  In tests, Files.walk() direct and indirect callers fail to close the returned Stream. Thanks to Adam Rauch, Gary Gregory. 
+o IO-811:  FileUtils.listFiles(File, String[], boolean) fails to close its internal Stream. Thanks to Adam Rauch, Gary Gregory. 
+o IO-811:  FileUtils.iterateFiles(File, String[], boolean) fails to close its internal Stream. Thanks to Adam Rauch, Gary Gregory. 
+o IO-811:  StreamIterator fails to close its internal Stream. Thanks to Adam Rauch, Gary Gregory. 
+o IO-814:  Don't throw UncheckedIOException #491. Thanks to Elliotte Rusty Harold, Gary Gregory. 
+o IO-414:  Don't write a BOM on every (or any) line #492. Thanks to Elliotte Rusty Harold, Gary Gregory. 
+o IO-814:  RandomAccessFileMode.create(Path) provides a better NullPointerException message. Thanks to Gary Gregory. 
+o           Improve performance of PathUtils.fileContentEquals(Path, Path, LinkOption[], OpenOption[]) by about 60%, see PathUtilsContentEqualsBenchmark. Thanks to Gary Gregory. 
+o           Improve performance of PathUtils.fileContentEquals(Path, Path) by about 60%, see PathUtilsContentEqualsBenchmark. Thanks to Gary Gregory. 
+o           Improve performance of FileUtils.contentEquals(File, File) by about 60%, see PathUtilsContentEqualsBenchmark. Thanks to Gary Gregory. 
+o           Remove unused test code #494. Thanks to Elliotte Rusty Harold. 
+o           [Javadoc] IOUtils#contentEquals does not throw NullPointerException #496. Thanks to sebbASF. 
+o           Fix CodeQL warnings in UnsynchronizedBufferedInputStream: Implicit narrowing conversion in compound assignment. Thanks to Gary Gregory. 
+o           MessageDigestCalculatingInputStream.MessageDigestMaintainingObserver.MessageDigestMaintainingObserver(MessageDigest) now throws a NullPointerException
+        if the MessageDigest is null. Thanks to Gary Gregory. 
+o           MessageDigestCalculatingInputStream.MessageDigestCalculatingInputStream(InputStream, MessageDigest) now throws a NullPointerException
+        if the MessageDigest is null. Thanks to Gary Gregory. 
+o IO-816:  UnsynchronizedBufferedInputStream.read(byte[], int, int) does not use buffer. Thanks to Andreas Loth, Gary Gregory. 
+
+Changes:
+o           Bump org.apache.commons:commons-parent from 62 to 64. Thanks to Gary Gregory. 
+
+Compatibility with 2.6:
+Binary compatible: Yes.
+Source compatible: Yes.
+Semantic compatible: Yes.
+
+Commons IO 2.9.0 requires Java 8.
+Commons IO 2.8.0 requires Java 8.
+Commons IO 2.7 requires Java 8.
+Commons IO 2.6 requires Java 7.
+Commons IO 2.5 requires Java 6.
+Commons IO 2.4 requires Java 6.
+Commons IO 2.3 requires Java 6.
+Commons IO 2.2 requires Java 5.
+Commons IO 1.4 requires Java 1.3.
+
+Historical list of changes: https://commons.apache.org/proper/commons-io/changes-report.html
+
+For complete information on Apache Commons IO, including instructions on how to submit bug reports,
+patches, or suggestions for improvement, see the Apache Commons IO website:
+
+https://commons.apache.org/proper/commons-io/
+
+Download page: https://commons.apache.org/proper/commons-io/download_io.cgi
+
+Have fun!
+-Apache Commons Team
+
+==============================================================================
 
 Apache Commons IO 
 Version 2.14.0
diff --git a/pom.xml b/pom.xml
index 18c11d9..bad8c9b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
    Licensed to the Apache Software Foundation (ASF) under one or more
    contributor license agreements.  See the NOTICE file distributed with
@@ -19,12 +19,12 @@
   <parent>
     <groupId>org.apache.commons</groupId>
     <artifactId>commons-parent</artifactId>
-    <version>62</version>
+    <version>64</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
   <groupId>commons-io</groupId>
   <artifactId>commons-io</artifactId>
-  <version>2.14.0</version>
+  <version>2.15.0</version>
   <name>Apache Commons IO</name>
 
   <inceptionYear>2002</inceptionYear>
@@ -52,7 +52,7 @@
     <connection>scm:git:https://gitbox.apache.org/repos/asf/commons-io.git</connection>
     <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/commons-io.git</developerConnection>
     <url>https://gitbox.apache.org/repos/asf?p=commons-io.git</url>
-    <tag>rel/commons-io-2.13.0</tag>
+    <tag>rel/commons-io-2.15.0</tag>
   </scm>
 
   <developers>
@@ -257,6 +257,20 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <!-- Java 21 support, revisit for Mockito 5 -->
+      <groupId>net.bytebuddy</groupId>
+      <artifactId>byte-buddy</artifactId>
+      <version>${commons.bytebuddy.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <!-- Java 21 support, revisit for Mockito 5 -->
+      <groupId>net.bytebuddy</groupId>
+      <artifactId>byte-buddy-agent</artifactId>
+      <version>${commons.bytebuddy.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-inline</artifactId>
       <version>4.11.0</version>
@@ -275,6 +289,12 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+      <version>1.16.0</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.openjdk.jmh</groupId>
       <artifactId>jmh-core</artifactId>
       <version>${jmh.version}</version>
@@ -294,8 +314,9 @@
     <commons.componentid>io</commons.componentid>
     <commons.module.name>org.apache.commons.io</commons.module.name>
     <commons.rc.version>RC1</commons.rc.version>
-    <commons.bc.version>2.13.0</commons.bc.version>
-    <commons.release.version>2.14.0</commons.release.version>
+    <commons.bc.version>2.14.0</commons.bc.version>
+    <commons.release.version>2.15.0</commons.release.version>
+    <commons.release.next>2.15.1</commons.release.next>
     <commons.release.desc>(requires Java 8)</commons.release.desc>
     <commons.jira.id>IO</commons.jira.id>
     <commons.jira.pid>12310477</commons.jira.pid>
@@ -325,11 +346,10 @@
     <commons.javadoc.java.link>${commons.javadoc8.java.link}</commons.javadoc.java.link>
     <commons.moditect.version>1.0.0.Final</commons.moditect.version>
     <jmh.version>1.37</jmh.version>
+    <commons.bytebuddy.version>1.14.9</commons.bytebuddy.version>
     <japicmp.skip>false</japicmp.skip>
     <jacoco.skip>${env.JACOCO_SKIP}</jacoco.skip>
     <commons.release.isDistModule>true</commons.release.isDistModule>
-    <commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>
-    <commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>
   </properties>
 
   <build>
@@ -389,7 +409,8 @@
           </classpathDependencyExcludes>
           <forkCount>1</forkCount>
           <reuseForks>false</reuseForks>
-          <!-- limit memory size see IO-161 -->
+          <!-- Limit memory size see IO-161 -->
+          <!-- Mockito inline may need -XX:+EnableDynamicAgentLoading -->
           <argLine>${argLine} -Xmx25M</argLine>
           <includes>
             <!-- Only include test classes, not test data -->
@@ -436,6 +457,14 @@
       <plugin>
         <groupId>com.github.siom79.japicmp</groupId>
         <artifactId>japicmp-maven-plugin</artifactId>
+        <configuration>
+          <parameter>
+			 <excludes>
+               <!-- False positive: https://github.com/siom79/japicmp/issues/365 -->
+               <exclude>org.apache.commons.io.StreamIterator</exclude>
+             </excludes>
+          </parameter>
+        </configuration>
       </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -465,7 +494,15 @@
       <plugin>
         <groupId>com.github.siom79.japicmp</groupId>
         <artifactId>japicmp-maven-plugin</artifactId>
-      </plugin>    
+        <configuration>
+          <parameter>
+			 <excludes>
+               <!-- False positive: https://github.com/siom79/japicmp/issues/365 -->
+               <exclude>org.apache.commons.io.StreamIterator</exclude>
+             </excludes>
+          </parameter>
+        </configuration>
+      </plugin>
     </plugins>
   </reporting>
   <profiles>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 28efbb7..d8665e8 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -47,7 +47,92 @@
   </properties>
 
   <body>
-    <release version="2.14.0" date="2023-09-24" description="Java 8 required.">
+    <release version="2.15.0" date="2023-10-21" description="Java 8 is required.">
+      <!-- FIX -->
+      <action dev="sebb" type="fix" issue="IO-810" due-to="Laurence Gonsalves">
+        XmlStreamReader encoding match RE is too strict.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-810" due-to="Gregor Dschung, Gary Gregory">
+        Javadoc in FileUtils does not reflect code for thrown exceptions.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-812" due-to="Adam Rauch, Gary Gregory">
+        Javadoc should mention closing Streams based on file resources.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-811" due-to="Adam Rauch, Gary Gregory">
+        In tests, Files.walk() direct and indirect callers fail to close the returned Stream.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-811" due-to="Adam Rauch, Gary Gregory">
+        FileUtils.listFiles(File, String[], boolean) fails to close its internal Stream.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-811" due-to="Adam Rauch, Gary Gregory">
+        FileUtils.iterateFiles(File, String[], boolean) fails to close its internal Stream.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-811" due-to="Adam Rauch, Gary Gregory">
+        StreamIterator fails to close its internal Stream.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-814" due-to="Elliotte Rusty Harold, Gary Gregory">
+        Don't throw UncheckedIOException #491.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-414" due-to="Elliotte Rusty Harold, Gary Gregory">
+        Don't write a BOM on every (or any) line #492.
+      </action>
+      <action dev="ggregory" type="fix" issue="IO-814" due-to="Gary Gregory">
+        RandomAccessFileMode.create(Path) provides a better NullPointerException message.         
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Improve performance of PathUtils.fileContentEquals(Path, Path, LinkOption[], OpenOption[]) by about 60%, see PathUtilsContentEqualsBenchmark.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Improve performance of PathUtils.fileContentEquals(Path, Path) by about 60%, see PathUtilsContentEqualsBenchmark.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Improve performance of FileUtils.contentEquals(File, File) by about 60%, see PathUtilsContentEqualsBenchmark.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Elliotte Rusty Harold">
+        Remove unused test code #494.
+      </action>
+      <action dev="ggregory" type="fix" due-to="sebbASF">
+        [Javadoc] IOUtils#contentEquals does not throw NullPointerException #496.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        Fix CodeQL warnings in UnsynchronizedBufferedInputStream: Implicit narrowing conversion in compound assignment.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        MessageDigestCalculatingInputStream.MessageDigestMaintainingObserver.MessageDigestMaintainingObserver(MessageDigest) now throws a NullPointerException
+        if the MessageDigest is null.
+      </action>
+      <action dev="ggregory" type="fix" due-to="Gary Gregory">
+        MessageDigestCalculatingInputStream.MessageDigestCalculatingInputStream(InputStream, MessageDigest) now throws a NullPointerException
+        if the MessageDigest is null.
+      </action>
+      <action issue="IO-816" dev="ggregory" type="fix" due-to="Andreas Loth, Gary Gregory">
+        UnsynchronizedBufferedInputStream.read(byte[], int, int) does not use buffer.
+      </action>
+      <!-- ADD -->
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add org.apache.commons.io.channels.FileChannels.
+      </action>
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add RandomAccessFiles#contentEquals(RandomAccessFile, RandomAccessFile).
+      </action>
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add RandomAccessFiles#reset(RandomAccessFile).
+      </action>
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add PathUtilsContentEqualsBenchmark.
+      </action>
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add org.apache.commons.io.StreamIterator.
+      </action>
+      <action dev="ggregory" type="add" due-to="Gary Gregory">
+        Add MessageDigestInputStream and deprecate MessageDigestCalculatingInputStream.
+      </action>
+      <!-- UPDATE -->
+      <action dev="ggregory" type="update" due-to="Gary Gregory">
+        Bump org.apache.commons:commons-parent from 62 to 64.
+      </action>
+    </release>
+    <release version="2.14.0" date="2023-09-24" description="Java 8 is required.">
       <!-- FIX -->
       <action dev="ggregory" type="fix" issue="IO-799" due-to="Jeroen van der Vegt, Gary Gregory">
         ReaderInputStream.read() throws an exception instead of returning -1 when called again after returning -1.
@@ -133,7 +218,7 @@
         Bump commons-lang3 from 3.12 to 3.13.0.
       </action>
     </release>
-    <release version="2.13.0" date="2023-06-03" description="Java 8 required.">
+    <release version="2.13.0" date="2023-06-03" description="Java 8 is required.">
       <!-- FIX -->
       <action issue="IO-791" dev="ggregory" type="fix" due-to="Chad Wilson, Gary Gregory">
         Regression in FileUtils.touch() - no longer creates parent directories.
@@ -204,7 +289,7 @@
         Bump commons-parent from 57 to 58.
       </action>
     </release>
-    <release version="2.12.0" date="2023-05-13" description="Java 8 required.">
+    <release version="2.12.0" date="2023-05-13" description="Java 8 is required.">
       <!-- FIX -->
       <action issue="IO-697" dev="kinow" type="fix" due-to="otter606">
         IOUtils.toByteArray size validation does not match documentation.
@@ -741,7 +826,7 @@
         Bump default buffer size for WriterOutputStream to IOUtils#DEFAULT_BUFFER_SIZE.
       </action>
     </release>
-    <release version="2.11.0" date="2021-07-09" description="Java 8 required.">
+    <release version="2.11.0" date="2021-07-09" description="Java 8 is required.">
       <!-- FIX -->
       <action issue="IO-741" dev="ggregory" type="fix" due-to="Zach Sherman">
         FileUtils.listFiles does not list matching files if File parameter is a symbolic link.
@@ -793,7 +878,7 @@
       </action>
     </release>
     <!-- The release date is the date RC is cut -->
-    <release version="2.10.0" date="2021-06-10" description="Java 8 required.">
+    <release version="2.10.0" date="2021-06-10" description="Java 8 is required.">
       <!-- FIX -->
       <action issue="IO-733" dev="ggregory" type="fix" due-to="Jim Sellers, Gary Gregory">
         RegexFileFilter uses the path and file name instead of just the file name.
@@ -828,7 +913,7 @@
         Bump mockito-inline from 3.10.0 to 3.11.0 #242.
       </action>
     </release>
-    <release version="2.9.0" date="2021-05-22" description="Java 8 required.">
+    <release version="2.9.0" date="2021-05-22" description="Java 8 is required.">
       <!-- FIX -->
       <action issue="IO-686" dev="ggregory" type="fix" due-to="Alan Moffat, Gary Gregory">
         IOUtils.toByteArray(InputStream) Javadoc does not match code.
@@ -1073,7 +1158,7 @@
       </action>
     </release>
     <!-- The release date is the date RC is cut -->
-    <release version="2.8.0" date="2020-09-05" description="Java 8 required.">
+    <release version="2.8.0" date="2020-09-05" description="Java 8 is required.">
       <action dev="ggregory" type="add" due-to="Gary Gregory">
         Add org.apache.commons.io.input.CircularInputStream.
       </action>
@@ -1199,7 +1284,7 @@
       </action>
     </release>
     <!-- The release date is the date RC is cut -->
-    <release version="2.7" date="2020-05-24" description="Java 8 required.">
+    <release version="2.7" date="2020-05-24" description="Java 8 is required.">
       <action issue="IO-589" dev="sebb" type="fix">
         Some tests fail if the base path contains a space.
       </action>
diff --git a/src/conf/checkstyle.xml b/src/conf/checkstyle.xml
index d2fc392..4ff5233 100644
--- a/src/conf/checkstyle.xml
+++ b/src/conf/checkstyle.xml
@@ -34,6 +34,12 @@
     <module name="AvoidStarImport" />
     <module name="RedundantImport" />
     <module name="UnusedImports" />
+    <module name="ImportOrder">
+      <property name="option" value="top"/>
+      <property name="groups" value="java,javax,org"/>
+      <property name="ordered" value="true"/>
+      <property name="separated" value="true"/>
+    </module>
     <module name="NeedBraces" />
     <module name="LeftCurly" />
     <module name="JavadocMethod" />
diff --git a/src/main/java/org/apache/commons/io/ByteOrderMark.java b/src/main/java/org/apache/commons/io/ByteOrderMark.java
index 2e4f030..c5ab57b 100644
--- a/src/main/java/org/apache/commons/io/ByteOrderMark.java
+++ b/src/main/java/org/apache/commons/io/ByteOrderMark.java
@@ -115,7 +115,14 @@
      */
     public static final char UTF_BOM = '\uFEFF';
 
+    /**
+     * Charset name.
+     */
     private final String charsetName;
+
+    /**
+     * Bytes.
+     */
     private final int[] bytes;
 
     /**
diff --git a/src/main/java/org/apache/commons/io/CopyUtils.java b/src/main/java/org/apache/commons/io/CopyUtils.java
index b7e7829..da01a98 100644
--- a/src/main/java/org/apache/commons/io/CopyUtils.java
+++ b/src/main/java/org/apache/commons/io/CopyUtils.java
@@ -103,7 +103,7 @@
  * method variants to specify the encoding, each row may
  * correspond to up to 2 methods.
  * <p>
- * Origin of code: Excalibur.
+ * Provenance: Excalibur.
  *
  * @deprecated Use IOUtils. Will be removed in 3.0.
  *  Methods renamed to IOUtils.write() or IOUtils.copy().
diff --git a/src/main/java/org/apache/commons/io/EndianUtils.java b/src/main/java/org/apache/commons/io/EndianUtils.java
index acc9531..83c2ce2 100644
--- a/src/main/java/org/apache/commons/io/EndianUtils.java
+++ b/src/main/java/org/apache/commons/io/EndianUtils.java
@@ -34,7 +34,7 @@
  * This class helps you solve this incompatibility.
  * </p>
  * <p>
- * Origin of code: Excalibur
+ * Provenance: Excalibur
  * </p>
  *
  * @see org.apache.commons.io.input.SwappedDataInputStream
diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java
index 807dc37..80d100f 100644
--- a/src/main/java/org/apache/commons/io/FileUtils.java
+++ b/src/main/java/org/apache/commons/io/FileUtils.java
@@ -36,6 +36,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.UnsupportedCharsetException;
 import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
 import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
@@ -108,7 +109,7 @@
  * {@link SecurityException} are not documented in the Javadoc.
  * </p>
  * <p>
- * Origin of code: Excalibur, Alexandria, Commons-Utils
+ * Provenance: Excalibur, Alexandria, Commons-Utils
  * </p>
  */
 public class FileUtils {
@@ -352,16 +353,13 @@
      * This method checks to see if the two files are different lengths or if they point to the same file, before
      * resorting to byte-by-byte comparison of the contents.
      * </p>
-     * <p>
-     * Code origin: Avalon
-     * </p>
      *
      * @param file1 the first file
      * @param file2 the second file
      * @return true if the content of the files are equal or they both don't exist, false otherwise
      * @throws IllegalArgumentException when an input is not a file.
      * @throws IOException If an I/O error occurs.
-     * @see org.apache.commons.io.file.PathUtils#fileContentEquals(Path,Path,java.nio.file.LinkOption[],java.nio.file.OpenOption...)
+     * @see PathUtils#fileContentEquals(Path,Path)
      */
     public static boolean contentEquals(final File file1, final File file2) throws IOException {
         if (file1 == null && file2 == null) {
@@ -393,9 +391,7 @@
             return true;
         }
 
-        try (InputStream input1 = Files.newInputStream(file1.toPath()); InputStream input2 = Files.newInputStream(file2.toPath())) {
-            return IOUtils.contentEquals(input1, input2);
-        }
+        return PathUtils.fileContentEquals(file1.toPath(), file2.toPath());
     }
 
     /**
@@ -451,12 +447,12 @@
     }
 
     /**
-     * Converts a Collection containing java.io.File instances into array
+     * Converts a Collection containing {@link File} instances into array
      * representation. This is to account for the difference between
      * File.listFiles() and FileUtils.listFiles().
      *
-     * @param files a Collection containing java.io.File instances
-     * @return an array of java.io.File
+     * @param files a Collection containing {@link File} instances
+     * @return an array of {@link File}
      */
     public static File[] convertFileCollectionToFileArray(final Collection<File> files) {
         return files.toArray(EMPTY_FILE_ARRAY);
@@ -1449,7 +1445,7 @@
     /**
      * Returns a {@link File} representing the system temporary directory.
      *
-     * @return the system temporary directory.
+     * @return the system temporary directory as a File
      * @since 2.0
      */
     public static File getTempDirectory() {
@@ -1459,7 +1455,12 @@
     /**
      * Returns the path to the system temporary directory.
      *
-     * @return the path to the system temporary directory.
+     * WARNING: this method relies on the Java system property 'java.io.tmpdir'
+     * which may or may not have a trailing file separator.
+     * This can affect code that uses String processing to manipulate pathnames rather
+     * than the standard libary methods in classes such as {@link java.io.File}
+     *
+     * @return the path to the system temporary directory as a String
      * @since 2.0
      */
     public static String getTempDirectoryPath() {
@@ -1971,7 +1972,7 @@
      * @param dirFilter  optional filter to apply when finding subdirectories.
      *                   If this parameter is {@code null}, subdirectories will not be included in the
      *                   search. Use TrueFileFilter.INSTANCE to match all directories.
-     * @return an iterator of java.io.File for the matching files
+     * @return an iterator of {@link File} for the matching files
      * @see org.apache.commons.io.filefilter.FileFilterUtils
      * @see org.apache.commons.io.filefilter.NameFileFilter
      * @since 1.2
@@ -1988,14 +1989,14 @@
      * </p>
      *
      * @param directory  the directory to search in
-     * @param extensions an array of extensions, ex. {"java","xml"}. If this
+     * @param extensions an array of extensions, for example, {"java","xml"}. If this
      *                   parameter is {@code null}, all files are returned.
      * @param recursive  if true all subdirectories are searched as well
-     * @return an iterator of java.io.File with the matching files
+     * @return an iterator of {@link File} with the matching files
      * @since 1.2
      */
     public static Iterator<File> iterateFiles(final File directory, final String[] extensions, final boolean recursive) {
-        return Uncheck.apply(d -> streamFiles(d, recursive, extensions).iterator(), directory);
+        return StreamIterator.iterator(Uncheck.get(() -> streamFiles(directory, recursive, extensions)));
     }
 
     /**
@@ -2016,7 +2017,7 @@
      * @param dirFilter  optional filter to apply when finding subdirectories.
      *                   If this parameter is {@code null}, subdirectories will not be included in the
      *                   search. Use TrueFileFilter.INSTANCE to match all directories.
-     * @return an iterator of java.io.File for the matching files
+     * @return an iterator of {@link File} for the matching files
      * @see org.apache.commons.io.filefilter.FileFilterUtils
      * @see org.apache.commons.io.filefilter.NameFileFilter
      * @since 2.2
@@ -2216,28 +2217,30 @@
      * @param dirFilter  optional filter to apply when finding subdirectories.
      *                   If this parameter is {@code null}, subdirectories will not be included in the
      *                   search. Use {@link TrueFileFilter#INSTANCE} to match all directories.
-     * @return a collection of java.io.File with the matching files
+     * @return a collection of {@link File} with the matching files
      * @see org.apache.commons.io.filefilter.FileFilterUtils
      * @see org.apache.commons.io.filefilter.NameFileFilter
      */
     public static Collection<File> listFiles(final File directory, final IOFileFilter fileFilter, final IOFileFilter dirFilter) {
         final AccumulatorPathVisitor visitor = Uncheck
             .apply(d -> listAccumulate(d, FileFileFilter.INSTANCE.and(fileFilter), dirFilter, FileVisitOption.FOLLOW_LINKS), directory);
-        return visitor.getFileList().stream().map(Path::toFile).collect(Collectors.toList());
+        return toList(visitor.getFileList().stream().map(Path::toFile));
     }
 
     /**
-     * Finds files within a given directory (and optionally its subdirectories)
+     * Lists files within a given directory (and optionally its subdirectories)
      * which match an array of extensions.
      *
      * @param directory  the directory to search in
-     * @param extensions an array of extensions, ex. {"java","xml"}. If this
+     * @param extensions an array of extensions, for example, {"java","xml"}. If this
      *                   parameter is {@code null}, all files are returned.
      * @param recursive  if true all subdirectories are searched as well
-     * @return a collection of java.io.File with the matching files
+     * @return a collection of {@link File} with the matching files
      */
     public static Collection<File> listFiles(final File directory, final String[] extensions, final boolean recursive) {
-        return Uncheck.apply(d -> toList(streamFiles(d, recursive, extensions)), directory);
+        try (Stream<File> fileStream = Uncheck.get(() -> streamFiles(directory, recursive, extensions))) {
+            return toList(fileStream);
+        }
     }
 
     /**
@@ -2253,7 +2256,7 @@
      * @param dirFilter  optional filter to apply when finding subdirectories.
      *                   If this parameter is {@code null}, subdirectories will not be included in the
      *                   search. Use TrueFileFilter.INSTANCE to match all directories.
-     * @return a collection of java.io.File with the matching files
+     * @return a collection of {@link File} with the matching files
      * @see org.apache.commons.io.FileUtils#listFiles
      * @see org.apache.commons.io.filefilter.FileFilterUtils
      * @see org.apache.commons.io.filefilter.NameFileFilter
@@ -2264,7 +2267,7 @@
             directory);
         final List<Path> list = visitor.getFileList();
         list.addAll(visitor.getDirList());
-        return list.stream().map(Path::toFile).collect(Collectors.toList());
+        return toList(list.stream().map(Path::toFile));
     }
 
     /**
@@ -2570,9 +2573,8 @@
      * @param file the file to read, must not be {@code null}
      * @return the file contents, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @since 1.1
      */
     public static byte[] readFileToByteArray(final File file) throws IOException {
@@ -2587,9 +2589,8 @@
      * @param file the file to read, must not be {@code null}
      * @return the file contents, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @since 1.3.1
      * @deprecated 2.5 use {@link #readFileToString(File, Charset)} instead (and specify the appropriate encoding)
      */
@@ -2606,9 +2607,8 @@
      * @param charsetName the name of the requested charset, {@code null} means platform default
      * @return the file contents, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @since 2.3
      */
     public static String readFileToString(final File file, final Charset charsetName) throws IOException {
@@ -2622,9 +2622,8 @@
      * @param charsetName the name of the requested charset, {@code null} means platform default
      * @return the file contents, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
      * .UnsupportedEncodingException} in version 2.2 if the named charset is unavailable.
      * @since 2.3
@@ -2640,9 +2639,8 @@
      * @param file the file to read, must not be {@code null}
      * @return the list of Strings representing each line in the file, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @since 1.3
      * @deprecated 2.5 use {@link #readLines(File, Charset)} instead (and specify the appropriate encoding)
      */
@@ -2659,9 +2657,8 @@
      * @param charset the charset to use, {@code null} means platform default
      * @return the list of Strings representing each line in the file, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @since 2.3
      */
     public static List<String> readLines(final File file, final Charset charset) throws IOException {
@@ -2675,9 +2672,8 @@
      * @param charsetName the name of the requested charset, {@code null} means platform default
      * @return the list of Strings representing each line in the file, never {@code null}
      * @throws NullPointerException if file is {@code null}.
-     * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some
-     *         other reason cannot be opened for reading.
-     * @throws IOException if an I/O error occurs.
+     * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a
+     *         regular file, or for some other reason why the file cannot be opened for reading.
      * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io
      * .UnsupportedEncodingException} in version 2.2 if the named charset is unavailable.
      * @since 1.1
@@ -2861,9 +2857,8 @@
      *          {@code false} otherwise
      * @throws NullPointerException if sourceFile is {@code null}.
      * @throws NullPointerException if targetFile is {@code null}.
-     * @throws IOException if setting the last-modified time failed.
      */
-    private static boolean setTimes(final File sourceFile, final File targetFile) throws IOException {
+    private static boolean setTimes(final File sourceFile, final File targetFile) {
         Objects.requireNonNull(sourceFile, "sourceFile");
         Objects.requireNonNull(targetFile, "targetFile");
         try {
@@ -2967,14 +2962,17 @@
     }
 
     /**
-     * Streams over the files in a given directory (and optionally
-     * its subdirectories) which match an array of extensions.
+     * Streams over the files in a given directory (and optionally its subdirectories) which match an array of extensions.
+     * <p>
+     * The returned {@link Stream} may wrap one or more {@link DirectoryStream}s. When you require timely disposal of file system resources, use a
+     * {@code try}-with-resources block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed. Calling a
+     * closed stream causes a {@link IllegalStateException}.
+     * </p>
      *
      * @param directory  the directory to search in
      * @param recursive  if true all subdirectories are searched as well
-     * @param extensions an array of extensions, ex. {"java","xml"}. If this
-     *                   parameter is {@code null}, all files are returned.
-     * @return an iterator of java.io.File with the matching files
+     * @param extensions an array of extensions, for example, {"java","xml"}. If this parameter is {@code null}, all files are returned.
+     * @return a Stream of {@link File} for matching files.
      * @throws IOException if an I/O error is thrown when accessing the starting file.
      * @since 2.9.0
      */
@@ -3006,8 +3004,8 @@
         if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) {
             return null;
         }
-        final String filename = url.getFile().replace('/', File.separatorChar);
-        return new File(decodeUrl(filename));
+        final String fileName = url.getFile().replace('/', File.separatorChar);
+        return new File(decodeUrl(fileName));
     }
 
     /**
@@ -3048,6 +3046,15 @@
         return files;
     }
 
+    /**
+     * Consumes all of the given stream.
+     * <p>
+     * When called from a FileTreeWalker, the walker <em>closes</em> the stream because {@link FileTreeWalker#next()} calls {@code top.stream().close()}.
+     * </p>
+     *
+     * @param stream The stream to consume.
+     * @return a new List.
+     */
     private static List<File> toList(final Stream<File> stream) {
         return stream.collect(Collectors.toList());
     }
diff --git a/src/main/java/org/apache/commons/io/FilenameUtils.java b/src/main/java/org/apache/commons/io/FilenameUtils.java
index 09c62f7..049c3a7 100644
--- a/src/main/java/org/apache/commons/io/FilenameUtils.java
+++ b/src/main/java/org/apache/commons/io/FilenameUtils.java
@@ -90,7 +90,7 @@
  * currently running on.
  * </p>
  * <p>
- * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils.
+ * Provenance: Excalibur, Alexandria, Tomcat, Commons-Utils.
  * </p>
  *
  * @since 1.1
@@ -846,7 +846,7 @@
      * ~user               --&gt; 6           --&gt; named user (slash added)
      * //server/a/b/c.txt  --&gt; 9
      * ///a/b/c.txt        --&gt; -1          --&gt; error
-     * C:                  --&gt; 0           --&gt; valid filename as only null character and / are reserved characters
+     * C:                  --&gt; 0           --&gt; valid file name as only null character and / are reserved characters
      * </pre>
      * <p>
      * The output will be the same irrespective of the machine that the code is running on.
diff --git a/src/main/java/org/apache/commons/io/HexDump.java b/src/main/java/org/apache/commons/io/HexDump.java
index d7e5abd..0a53e3d 100644
--- a/src/main/java/org/apache/commons/io/HexDump.java
+++ b/src/main/java/org/apache/commons/io/HexDump.java
@@ -31,7 +31,7 @@
  * in hexadecimal form.
  * </p>
  * <p>
- * Origin of code: POI.
+ * Provenance: POI.
  * </p>
  */
 public class HexDump {
diff --git a/src/main/java/org/apache/commons/io/IOIndexedException.java b/src/main/java/org/apache/commons/io/IOIndexedException.java
index 9a85328..1893509 100644
--- a/src/main/java/org/apache/commons/io/IOIndexedException.java
+++ b/src/main/java/org/apache/commons/io/IOIndexedException.java
@@ -42,6 +42,9 @@
         return String.format("%s #%,d: %s", name, index, msg);
     }
 
+    /**
+     * Index.
+     */
     private final int index;
 
     /**
diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java
index b6b6f13..dfb4c19 100644
--- a/src/main/java/org/apache/commons/io/IOUtils.java
+++ b/src/main/java/org/apache/commons/io/IOUtils.java
@@ -45,6 +45,7 @@
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.Selector;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.util.Arrays;
 import java.util.Collection;
@@ -118,7 +119,7 @@
  * closing streams after use.
  * </p>
  * <p>
- * Origin of code: Excalibur.
+ * Provenance: Excalibur.
  * </p>
  */
 public class IOUtils {
@@ -893,7 +894,6 @@
      * @param input2 the second stream
      * @return true if the content of the streams are equal or they both don't
      * exist, false otherwise
-     * @throws NullPointerException if either input is null
      * @throws IOException          if an I/O error occurs
      */
     public static boolean contentEquals(final InputStream input1, final InputStream input2) throws IOException {
@@ -3827,28 +3827,36 @@
      * Writes the {@link #toString()} value of each item in a collection to
      * an {@link OutputStream} line by line, using the specified character
      * encoding and the specified line ending.
+     * <p>
+     * UTF-16 is written big-endian with no byte order mark.
+     * For little endian, use UTF-16LE. For a BOM, write it to the stream
+     * before calling this method.
+     * </p>
      *
      * @param lines the lines to write, null entries produce blank lines
      * @param lineEnding the line separator to use, null is system default
      * @param output the {@link OutputStream} to write to, not null, not closed
      * @param charset the charset to use, null means platform default
-     * @throws NullPointerException if the output is null
+     * @throws NullPointerException if output is null
      * @throws IOException          if an I/O error occurs
      * @since 2.3
      */
     public static void writeLines(final Collection<?> lines, String lineEnding, final OutputStream output,
-                                  final Charset charset) throws IOException {
+            Charset charset) throws IOException {
         if (lines == null) {
             return;
         }
         if (lineEnding == null) {
             lineEnding = System.lineSeparator();
         }
-        final Charset cs = Charsets.toCharset(charset);
-        final byte[] eolBytes = lineEnding.getBytes(cs);
+        if (StandardCharsets.UTF_16.equals(charset)) {
+            // don't write a BOM
+            charset = StandardCharsets.UTF_16BE;
+        }
+        final byte[] eolBytes = lineEnding.getBytes(charset);
         for (final Object line : lines) {
             if (line != null) {
-                write(line.toString(), output, cs);
+                write(line.toString(), output, charset);
             }
             output.write(eolBytes);
         }
diff --git a/src/main/java/org/apache/commons/io/RandomAccessFileMode.java b/src/main/java/org/apache/commons/io/RandomAccessFileMode.java
index 5406158..8b51e43 100644
--- a/src/main/java/org/apache/commons/io/RandomAccessFileMode.java
+++ b/src/main/java/org/apache/commons/io/RandomAccessFileMode.java
@@ -20,6 +20,7 @@
 import java.io.FileNotFoundException;
 import java.io.RandomAccessFile;
 import java.nio.file.Path;
+import java.util.Objects;
 
 /**
  * Access modes and factory methods for {@link RandomAccessFile}.
@@ -77,7 +78,7 @@
      * @throws FileNotFoundException See {@link RandomAccessFile#RandomAccessFile(File, String)}.
      */
     public RandomAccessFile create(final Path file) throws FileNotFoundException {
-        return create(file.toFile());
+        return create(Objects.requireNonNull(file.toFile(), "file"));
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/io/RandomAccessFiles.java b/src/main/java/org/apache/commons/io/RandomAccessFiles.java
index 2fc071f..0e0140b 100644
--- a/src/main/java/org/apache/commons/io/RandomAccessFiles.java
+++ b/src/main/java/org/apache/commons/io/RandomAccessFiles.java
@@ -19,15 +19,55 @@
 
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.nio.channels.FileChannel;
+import java.util.Objects;
+
+import org.apache.commons.io.channels.FileChannels;
 
 /**
- * Works on RandomAccessFile.
+ * Works with {@link RandomAccessFile}.
  *
  * @since 2.13.0
  */
 public class RandomAccessFiles {
 
     /**
+     * Tests if two RandomAccessFile contents are equal.
+     *
+     * @param raf1 A RandomAccessFile.
+     * @param raf2 Another RandomAccessFile.
+     * @return true if the contents of both RandomAccessFiles are equal, false otherwise.
+     * @throws IOException if an I/O error occurs.
+     * @since 2.15.0
+     */
+    @SuppressWarnings("resource") // See comments
+    public static boolean contentEquals(final RandomAccessFile raf1, final RandomAccessFile raf2) throws IOException {
+        // Short-circuit test
+        if (Objects.equals(raf1, raf2)) {
+            return true;
+        }
+        // Short-circuit test
+        final long length1 = length(raf1);
+        final long length2 = length(raf2);
+        if (length1 != length2) {
+            return false;
+        }
+        if (length1 == 0 && length2 == 0) {
+            return true;
+        }
+        // Dig in and to the work
+        // We do not close FileChannels because that closes the owning RandomAccessFile.
+        // Instead, the caller is assumed to manage the given RandomAccessFile objects.
+        final FileChannel channel1 = raf1.getChannel();
+        final FileChannel channel2 = raf2.getChannel();
+        return FileChannels.contentEquals(channel1, channel2, IOUtils.DEFAULT_BUFFER_SIZE);
+    }
+
+    private static long length(final RandomAccessFile raf) throws IOException {
+        return raf != null ? raf.length() : 0;
+    }
+
+    /**
      * Reads a byte array starting at "position" for "length" bytes.
      *
      * @param input    The source RandomAccessFile.
@@ -42,4 +82,17 @@
         return IOUtils.toByteArray(input::read, length);
     }
 
+    /**
+     * Resets the given file to position 0.
+     *
+     * @param raf The RandomAccessFile to reset.
+     * @return The given RandomAccessFile.
+     * @throws IOException If {@code pos} is less than {@code 0} or if an I/O error occurs.
+     * @since 2.15.0
+     */
+    public static RandomAccessFile reset(final RandomAccessFile raf) throws IOException {
+        raf.seek(0);
+        return raf;
+    }
+
 }
diff --git a/src/main/java/org/apache/commons/io/StreamIterator.java b/src/main/java/org/apache/commons/io/StreamIterator.java
index 3a32168..2346565 100644
--- a/src/main/java/org/apache/commons/io/StreamIterator.java
+++ b/src/main/java/org/apache/commons/io/StreamIterator.java
@@ -17,42 +17,55 @@
 
 package org.apache.commons.io;
 
-import java.io.Closeable;
 import java.util.Iterator;
 import java.util.Objects;
 import java.util.stream.Stream;
 
 /**
- * Wraps and presents a stream as a closable iterator resource that automatically closes itself when reaching the end
- * of stream.
+ * Wraps and presents a {@link Stream} as a {@link AutoCloseable} {@link Iterator} resource that automatically closes itself when reaching the end of stream.
  *
- * @param <E> The stream and iterator type.
- * @since 2.9.0
+ * <h2>Warning</h2>
+ * <p>
+ * In order to close the stream, the call site MUST either close the stream it allocated OR call this iterator until the end.
+ * </p>
+ *
+ * @param <E> The {@link Stream} and {@link Iterator} type.
+ * @since 2.15.0
  */
-final class StreamIterator<E> implements Iterator<E>, Closeable {
+public final class StreamIterator<E> implements Iterator<E>, AutoCloseable {
 
     /**
-     * Wraps and presents a stream as a closable resource that automatically closes itself when reaching the end of
-     * stream.
-     * <h2>Warning</h2>
+     * Wraps and presents a stream as a closable resource that automatically closes itself when reaching the end of stream.
      * <p>
-     * In order to close the stream, the call site MUST either close the stream it allocated OR call the iterator until
-     * the end.
+     * <b>Warning</b>
+     * </p>
+     * <p>
+     * In order to close the stream, the call site MUST either close the stream it allocated OR call this iterator until the end.
      * </p>
      *
-     * @param <T> The stream and iterator type.
+     * @param <T>    The stream and iterator type.
      * @param stream The stream iterate.
      * @return A new iterator.
      */
-    @SuppressWarnings("resource") // Caller MUST close or iterate to the end.
-    public static <T> Iterator<T> iterator(final Stream<T> stream) {
-        return new StreamIterator<>(stream).iterator;
+    public static <T> StreamIterator<T> iterator(final Stream<T> stream) {
+        return new StreamIterator<>(stream);
     }
 
+    /**
+     * The given stream's Iterator.
+     */
     private final Iterator<E> iterator;
 
+    /**
+     * The given stream.
+     */
     private final Stream<E> stream;
 
+    /**
+     * Whether {@link #close()} has been called.
+     */
+    private boolean closed;
+
     private StreamIterator(final Stream<E> stream) {
         this.stream = Objects.requireNonNull(stream, "stream");
         this.iterator = stream.iterator();
@@ -63,11 +76,16 @@
      */
     @Override
     public void close() {
+        closed = true;
         stream.close();
     }
 
     @Override
     public boolean hasNext() {
+        if (closed) {
+            // Calling Iterator#hasNext() on a closed java.nio.file.FileTreeIterator causes an IllegalStateException.
+            return false;
+        }
         final boolean hasNext = iterator.hasNext();
         if (!hasNext) {
             close();
diff --git a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java
index d94638a..e0f9ba6 100644
--- a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java
+++ b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java
@@ -154,6 +154,11 @@
         return checkOrigin().getInputStream(getOpenOptions());
     }
 
+    /**
+     * Gets the OpenOption.
+     *
+     * @return the OpenOption.
+     */
     protected OpenOption[] getOpenOptions() {
         return openOptions;
     }
diff --git a/src/main/java/org/apache/commons/io/channels/FileChannels.java b/src/main/java/org/apache/commons/io/channels/FileChannels.java
new file mode 100644
index 0000000..468c85b
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/channels/FileChannels.java
@@ -0,0 +1,87 @@
+/*
+ * 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.channels;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.Objects;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Works with {@link FileChannel}.
+ *
+ * @since 2.15.0
+ */
+public final class FileChannels {
+
+    /**
+     * Tests if two RandomAccessFiles contents are equal.
+     *
+     * @param channel1       A FileChannel.
+     * @param channel2       Another FileChannel.
+     * @param byteBufferSize The two internal buffer capacities, in bytes.
+     * @return true if the contents of both RandomAccessFiles are equal, false otherwise.
+     * @throws IOException if an I/O error occurs.
+     */
+    public static boolean contentEquals(final FileChannel channel1, final FileChannel channel2, final int byteBufferSize) throws IOException {
+        // Short-circuit test
+        if (Objects.equals(channel1, channel2)) {
+            return true;
+        }
+        // Short-circuit test
+        final long size1 = size(channel1);
+        final long size2 = size(channel2);
+        if (size1 != size2) {
+            return false;
+        }
+        if (size1 == 0 && size2 == 0) {
+            return true;
+        }
+        // Dig in and do the work
+        final ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(byteBufferSize);
+        final ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(byteBufferSize);
+        while (true) {
+            final int read1 = channel1.read(byteBuffer1);
+            final int read2 = channel2.read(byteBuffer2);
+            if (read1 == IOUtils.EOF && read2 == IOUtils.EOF) {
+                return byteBuffer1.equals(byteBuffer2);
+            }
+            if (read1 != read2) {
+                return false;
+            }
+            if (!byteBuffer1.equals(byteBuffer2)) {
+                return false;
+            }
+            byteBuffer1.clear();
+            byteBuffer2.clear();
+        }
+    }
+
+    private static long size(final FileChannel channel) throws IOException {
+        return channel != null ? channel.size() : 0;
+    }
+
+    /**
+     * Don't instantiate.
+     */
+    private FileChannels() {
+        // no-op
+    }
+}
diff --git a/src/main/java/org/apache/commons/io/channels/package-info.java b/src/main/java/org/apache/commons/io/channels/package-info.java
new file mode 100644
index 0000000..51a7ca2
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/channels/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides classes to work with {@link java.nio.channels}.
+ *
+ * @since 2.15.0
+ */
+package org.apache.commons.io.channels;
diff --git a/src/main/java/org/apache/commons/io/comparator/CompositeFileComparator.java b/src/main/java/org/apache/commons/io/comparator/CompositeFileComparator.java
index 0f57a83..d50f325 100644
--- a/src/main/java/org/apache/commons/io/comparator/CompositeFileComparator.java
+++ b/src/main/java/org/apache/commons/io/comparator/CompositeFileComparator.java
@@ -19,6 +19,7 @@
 import java.io.File;
 import java.io.Serializable;
 import java.util.Comparator;
+import java.util.function.IntFunction;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
@@ -48,6 +49,9 @@
     private static final Comparator<?>[] EMPTY_COMPARATOR_ARRAY = {};
     private static final long serialVersionUID = -2224170307287243428L;
 
+    /**
+     * Delegates.
+     */
     private final Comparator<File>[] delegates;
 
     /**
@@ -65,7 +69,8 @@
      * @param delegates The delegate file comparators
      */
     public CompositeFileComparator(final Iterable<Comparator<File>> delegates) {
-        this.delegates = delegates == null ? emptyArray() : StreamSupport.stream(delegates.spliterator(), false).toArray(Comparator[]::new);
+        this.delegates = delegates == null ? emptyArray()
+                : StreamSupport.stream(delegates.spliterator(), false).toArray((IntFunction<Comparator<File>[]>) Comparator[]::new);
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/io/file/FilesUncheck.java b/src/main/java/org/apache/commons/io/file/FilesUncheck.java
index 02a1feb..02688ca 100644
--- a/src/main/java/org/apache/commons/io/file/FilesUncheck.java
+++ b/src/main/java/org/apache/commons/io/file/FilesUncheck.java
@@ -22,6 +22,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.Charset;
@@ -244,6 +245,10 @@
 
     /**
      * Delegates to {@link Files#find(Path, int, BiPredicate, FileVisitOption...)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} wraps a {@link DirectoryStream}. When you require timely disposal of file system resources, use a {@code try}-with-resources
+     * block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed.
+     * </p>
      *
      * @param start    See delegate.
      * @param maxDepth See delegate.
@@ -348,6 +353,10 @@
 
     /**
      * Delegates to {@link Files#lines(Path)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} wraps a {@link Reader}. When you require timely disposal of file system resources, use a {@code try}-with-resources block to
+     * ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed.
+     * </p>
      *
      * @param path See delegate.
      * @return See delegate.
@@ -359,6 +368,10 @@
 
     /**
      * Delegates to {@link Files#lines(Path, Charset)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} wraps a {@link Reader}. When you require timely disposal of file system resources, use a {@code try}-with-resources block to
+     * ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed.
+     * </p>
      *
      * @param path See delegate.
      * @param cs See delegate.
@@ -371,6 +384,10 @@
 
     /**
      * Delegates to {@link Files#list(Path)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} wraps a {@link DirectoryStream}. When you require timely disposal of file system resources, use a {@code try}-with-resources
+     * block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed.
+     * </p>
      *
      * @param dir See delegate.
      * @return See delegate.
@@ -474,8 +491,11 @@
     }
 
     /**
-     * Delegates to {@link Files#newDirectoryStream(Path)} throwing {@link UncheckedIOException} instead of
-     * {@link IOException}.
+     * Delegates to {@link Files#newDirectoryStream(Path)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * If you don't use the try-with-resources construct, then you must call the stream's {@link Stream#close()} method after iteration is complete to free any
+     * resources held for the open directory.
+     * </p>
      *
      * @param dir See delegate.
      * @return See delegate.
@@ -485,10 +505,14 @@
     }
 
     /**
-     * Delegates to {@link Files#newDirectoryStream(Path, java.nio.file.DirectoryStream.Filter)} throwing
-     * {@link UncheckedIOException} instead of {@link IOException}.
+     * Delegates to {@link Files#newDirectoryStream(Path, java.nio.file.DirectoryStream.Filter)} throwing {@link UncheckedIOException} instead of
+     * {@link IOException}.
+     * <p>
+     * If you don't use the try-with-resources construct, then you must call the stream's {@link Stream#close()} method after iteration is complete to free any
+     * resources held for the open directory.
+     * </p>
      *
-     * @param dir See delegate.
+     * @param dir    See delegate.
      * @param filter See delegate.
      * @return See delegate.
      */
@@ -499,6 +523,10 @@
     /**
      * Delegates to {@link Files#newDirectoryStream(Path, String)} throwing {@link UncheckedIOException} instead of
      * {@link IOException}.
+     * <p>
+     * The returned {@link Stream} wraps a {@link DirectoryStream}. When you require timely disposal of file system resources, use a {@code try}-with-resources
+     * block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed.
+     * </p>
      *
      * @param dir See delegate.
      * @param glob See delegate.
@@ -674,10 +702,14 @@
     }
 
     /**
-     * Delegates to {@link Files#walk(Path, FileVisitOption...)} throwing {@link UncheckedIOException} instead of
-     * {@link IOException}.
+     * Delegates to {@link Files#walk(Path, FileVisitOption...)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} may wrap one or more {@link DirectoryStream}s. When you require timely disposal of file system resources, use a
+     * {@code try}-with-resources block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed. Calling a
+     * closed stream causes a {@link IllegalStateException}.
+     * </p>
      *
-     * @param start See delegate.
+     * @param start   See delegate.
      * @param options See delegate.
      * @return See delegate.
      */
@@ -686,12 +718,16 @@
     }
 
     /**
-     * Delegates to {@link Files#walk(Path, int, FileVisitOption...)} throwing {@link UncheckedIOException} instead of
-     * {@link IOException}.
+     * Delegates to {@link Files#walk(Path, int, FileVisitOption...)} throwing {@link UncheckedIOException} instead of {@link IOException}.
+     * <p>
+     * The returned {@link Stream} may wrap one or more {@link DirectoryStream}s. When you require timely disposal of file system resources, use a
+     * {@code try}-with-resources block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed. Calling a
+     * closed stream causes a {@link IllegalStateException}.
+     * </p>
      *
-     * @param start See delegate.
+     * @param start    See delegate.
      * @param maxDepth See delegate.
-     * @param options See delegate.
+     * @param options  See delegate.
      * @return See delegate.
      */
     public static Stream<Path> walk(final Path start, final int maxDepth, final FileVisitOption... options) {
diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java b/src/main/java/org/apache/commons/io/file/PathUtils.java
index 56b2a05..d85fdb6 100644
--- a/src/main/java/org/apache/commons/io/file/PathUtils.java
+++ b/src/main/java/org/apache/commons/io/file/PathUtils.java
@@ -21,7 +21,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.UncheckedIOException;
+import java.io.RandomAccessFile;
 import java.math.BigInteger;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -70,14 +70,14 @@
 import org.apache.commons.io.Charsets;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.RandomAccessFileMode;
+import org.apache.commons.io.RandomAccessFiles;
 import org.apache.commons.io.ThreadUtils;
 import org.apache.commons.io.file.Counters.PathCounters;
 import org.apache.commons.io.file.attribute.FileTimes;
 import org.apache.commons.io.filefilter.IOFileFilter;
 import org.apache.commons.io.function.IOFunction;
 import org.apache.commons.io.function.IOSupplier;
-import org.apache.commons.io.function.Uncheck;
 
 /**
  * NIO Path utilities.
@@ -90,7 +90,7 @@
      * Private worker/holder that computes and tracks relative path names and their equality. We reuse the sorted relative
      * lists when comparing directories.
      */
-    private static class RelativeSortedPaths {
+    private static final class RelativeSortedPaths {
 
         final boolean equals;
         // final List<Path> relativeDirList1; // might need later?
@@ -365,7 +365,7 @@
      * Creates the parent directories for the given {@code path}.
      * <p>
      * If the parent directory already exists, then return it.
-     * <p>
+     * </p>
      *
      * @param path The path to a file (or directory).
      * @param attrs An optional list of file attributes to set atomically when creating the directories.
@@ -381,7 +381,7 @@
      * Creates the parent directories for the given {@code path}.
      * <p>
      * If the parent directory already exists, then return it.
-     * <p>
+     * </p>
      *
      * @param path The path to a file (or directory).
      * @param linkOption A {@link LinkOption} or null.
@@ -719,20 +719,20 @@
     /**
      * Compares the file contents of two Paths to determine if they are equal or not.
      * <p>
-     * File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
+     * File content is accessed through {@link RandomAccessFileMode#create(Path)}.
      * </p>
      *
      * @param path1 the first stream.
      * @param path2 the second stream.
      * @param linkOptions options specifying how files are followed.
-     * @param openOptions options specifying how files are opened.
+     * @param openOptions ignored.
      * @return true if the content of the streams are equal or they both don't exist, false otherwise.
      * @throws NullPointerException if openOptions is null.
      * @throws IOException if an I/O error occurs.
      * @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
      */
     public static boolean fileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions, final OpenOption[] openOptions)
-        throws IOException {
+            throws IOException {
         if (path1 == null && path2 == null) {
             return true;
         }
@@ -766,9 +766,9 @@
             // same file
             return true;
         }
-        try (InputStream inputStream1 = Files.newInputStream(nPath1, openOptions);
-            InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) {
-            return IOUtils.contentEquals(inputStream1, inputStream2);
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(path1.toRealPath(linkOptions));
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(path2.toRealPath(linkOptions))) {
+            return RandomAccessFiles.contentEquals(raf1, raf2);
         }
     }
 
@@ -1182,8 +1182,12 @@
 
     /**
      * Creates a new DirectoryStream for Paths rooted at the given directory.
+     * <p>
+     * If you don't use the try-with-resources construct, then you must call the stream's {@link Stream#close()} method after iteration is complete to free any
+     * resources held for the open directory.
+     * </p>
      *
-     * @param dir the path to the directory to stream.
+     * @param dir        the path to the directory to stream.
      * @param pathFilter the directory stream filter.
      * @return a new instance.
      * @throws IOException if an I/O error occurs.
@@ -1243,21 +1247,20 @@
     }
 
     /**
-     * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
-     * {@link UnsupportedOperationException}. Throws {@link Uncheck} instead of {@link IOException}.
+     * Reads the BasicFileAttributes from the given path. Returns null if the attributes can't be read.
      *
      * @param <A> The {@link BasicFileAttributes} type
      * @param path The Path to test.
      * @param type the {@link Class} of the file attributes required to read.
      * @param options options indicating how to handle symbolic links.
-     * @return the file attributes.
+     * @return the file attributes or null if the attributes can't be read.
      * @see Files#readAttributes(Path, Class, LinkOption...)
      * @since 2.12.0
      */
     public static <A extends BasicFileAttributes> A readAttributes(final Path path, final Class<A> type, final LinkOption... options) {
         try {
-            return path == null ? null : Uncheck.apply(Files::readAttributes, path, type, options);
-        } catch (final UnsupportedOperationException e) {
+            return path == null ? null : Files.readAttributes(path, type, options);
+        } catch (final UnsupportedOperationException | IOException e) {
             // For example, on Windows.
             return null;
         }
@@ -1270,16 +1273,14 @@
      * @return the path attributes.
      * @throws IOException if an I/O error occurs.
      * @since 2.9.0
-     * @deprecated Will be removed in 3.0.0 in favor of {@link #readBasicFileAttributes(Path, LinkOption...)}.
      */
-    @Deprecated
     public static BasicFileAttributes readBasicFileAttributes(final Path path) throws IOException {
         return Files.readAttributes(path, BasicFileAttributes.class);
     }
 
     /**
-     * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
-     * {@link UnsupportedOperationException}.
+     * Reads the BasicFileAttributes from the given path. Returns null if the attributes
+     * can't be read.
      *
      * @param path the path to read.
      * @param options options indicating how to handle symbolic links.
@@ -1291,12 +1292,11 @@
     }
 
     /**
-     * Reads the BasicFileAttributes from the given path. Returns null instead of throwing
-     * {@link UnsupportedOperationException}.
+     * Reads the BasicFileAttributes from the given path. Returns null if the attributes
+     * can't be read.
      *
      * @param path the path to read.
      * @return the path attributes.
-     * @throws UncheckedIOException if an I/O error occurs
      * @since 2.9.0
      * @deprecated Use {@link #readBasicFileAttributes(Path, LinkOption...)}.
      */
@@ -1306,8 +1306,8 @@
     }
 
     /**
-     * Reads the DosFileAttributes from the given path. Returns null instead of throwing
-     * {@link UnsupportedOperationException}.
+     * Reads the DosFileAttributes from the given path. Returns null if the attributes
+     * can't be read.
      *
      * @param path the path to read.
      * @param options options indicating how to handle symbolic links.
@@ -1323,8 +1323,8 @@
     }
 
     /**
-     * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null instead of throwing
-     * {@link UnsupportedOperationException}.
+     * Reads the PosixFileAttributes or DosFileAttributes from the given path. Returns null if the attributes
+     * can't be read.
      *
      * @param path The Path to read.
      * @param options options indicating how to handle symbolic links.
@@ -1751,6 +1751,11 @@
 
     /**
      * Returns a stream of filtered paths.
+     * <p>
+     * The returned {@link Stream} may wrap one or more {@link DirectoryStream}s. When you require timely disposal of file system resources, use a
+     * {@code try}-with-resources block to ensure invocation of the stream's {@link Stream#close()} method after the stream operations are completed. Calling a
+     * closed stream causes a {@link IllegalStateException}.
+     * </p>
      *
      * @param start the start path
      * @param pathFilter the path filter
@@ -1801,7 +1806,7 @@
     }
 
     /**
-     * Does allow to instantiate.
+     * Prevents instantiation.
      */
     private PathUtils() {
         // do not instantiate.
diff --git a/src/main/java/org/apache/commons/io/filefilter/DelegateFileFilter.java b/src/main/java/org/apache/commons/io/filefilter/DelegateFileFilter.java
index 14c9956..ee2bb02 100644
--- a/src/main/java/org/apache/commons/io/filefilter/DelegateFileFilter.java
+++ b/src/main/java/org/apache/commons/io/filefilter/DelegateFileFilter.java
@@ -39,7 +39,7 @@
     /** The File filter */
     private final FileFilter fileFilter;
     /** The Filename filter */
-    private final FilenameFilter filenameFilter;
+    private final FilenameFilter fileNameFilter;
 
     /**
      * Constructs a delegate file filter around an existing FileFilter.
@@ -49,17 +49,17 @@
     public DelegateFileFilter(final FileFilter fileFilter) {
         Objects.requireNonNull(fileFilter, "filter");
         this.fileFilter = fileFilter;
-        this.filenameFilter = null;
+        this.fileNameFilter = null;
     }
 
     /**
      * Constructs a delegate file filter around an existing FilenameFilter.
      *
-     * @param filenameFilter  the filter to decorate
+     * @param fileNameFilter  the filter to decorate
      */
-    public DelegateFileFilter(final FilenameFilter filenameFilter) {
-        Objects.requireNonNull(filenameFilter, "filter");
-        this.filenameFilter = filenameFilter;
+    public DelegateFileFilter(final FilenameFilter fileNameFilter) {
+        Objects.requireNonNull(fileNameFilter, "filter");
+        this.fileNameFilter = fileNameFilter;
         this.fileFilter = null;
     }
 
@@ -86,8 +86,8 @@
      */
     @Override
     public boolean accept(final File dir, final String name) {
-        if (filenameFilter != null) {
-            return filenameFilter.accept(dir, name);
+        if (fileNameFilter != null) {
+            return fileNameFilter.accept(dir, name);
         }
         return super.accept(dir, name);
     }
@@ -99,7 +99,7 @@
      */
     @Override
     public String toString() {
-        final String delegate = fileFilter != null ? fileFilter.toString() : filenameFilter.toString();
+        final String delegate = fileFilter != null ? fileFilter.toString() : fileNameFilter.toString();
         return super.toString() + "(" + delegate + ")";
     }
 
diff --git a/src/main/java/org/apache/commons/io/filefilter/package-info.java b/src/main/java/org/apache/commons/io/filefilter/package-info.java
index a374fb0..3bf7123 100644
--- a/src/main/java/org/apache/commons/io/filefilter/package-info.java
+++ b/src/main/java/org/apache/commons/io/filefilter/package-info.java
@@ -40,7 +40,7 @@
  * </tr>
  * <tr>
  * <td><a href="NameFileFilter.html">NameFileFilter</a></td>
- * <td>Filter based on a filename</td>
+ * <td>Filter based on a file name</td>
  * </tr>
  * <tr>
  * <td><a href="WildcardFileFilter.html">WildcardFileFilter</a></td>
diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java
index fc334e9..72b1b1e 100644
--- a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java
+++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java
@@ -38,7 +38,7 @@
         void clean(ByteBuffer buffer) throws ReflectiveOperationException;
     }
 
-    private static class Java8Cleaner implements Cleaner {
+    private static final class Java8Cleaner implements Cleaner {
 
         private final Method cleanerMethod;
         private final Method cleanMethod;
@@ -57,7 +57,7 @@
         }
     }
 
-    private static class Java9Cleaner implements Cleaner {
+    private static final class Java9Cleaner implements Cleaner {
 
         private final Object theUnsafe;
         private final Method invokeCleaner;
diff --git a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java
index 89114be..3d03f8c 100644
--- a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java
@@ -87,6 +87,9 @@
      */
     public static class Builder extends AbstractStreamBuilder<MemoryMappedFileInputStream, Builder> {
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             setBufferSizeDefault(DEFAULT_BUFFER_SIZE);
             setBufferSize(DEFAULT_BUFFER_SIZE);
diff --git a/src/main/java/org/apache/commons/io/input/MessageDigestCalculatingInputStream.java b/src/main/java/org/apache/commons/io/input/MessageDigestCalculatingInputStream.java
index 7aed418..97d7ff5 100644
--- a/src/main/java/org/apache/commons/io/input/MessageDigestCalculatingInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/MessageDigestCalculatingInputStream.java
@@ -21,12 +21,13 @@
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.Provider;
+import java.util.Objects;
 
 import org.apache.commons.io.build.AbstractStreamBuilder;
 
 /**
  * This class is an example for using an {@link ObservableInputStream}. It creates its own {@link org.apache.commons.io.input.ObservableInputStream.Observer},
- * which calculates a checksum using a MessageDigest, for example an MD5 sum.
+ * which calculates a checksum using a {@link MessageDigest}, for example, a SHA-512 sum.
  * <p>
  * To build an instance, see {@link Builder}.
  * </p>
@@ -35,12 +36,11 @@
  * Cryptography Architecture Standard Algorithm Name Documentation</a> for information about standard algorithm names.
  * </p>
  * <p>
- * <em>Note</em>: Neither {@link ObservableInputStream}, nor {@link MessageDigest}, are thread safe. So is {@link MessageDigestCalculatingInputStream}.
+ * <em>Note</em>: Neither {@link ObservableInputStream}, nor {@link MessageDigest}, are thread safe, so is {@link MessageDigestCalculatingInputStream}.
  * </p>
- * <p>
- * TODO Rename to MessageDigestInputStream in 3.0.
- * </p>
+ * @deprecated Use {@link MessageDigestInputStream}.
  */
+@Deprecated
 public class MessageDigestCalculatingInputStream extends ObservableInputStream {
 
     /**
@@ -61,6 +61,9 @@
 
         private MessageDigest messageDigest;
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             try {
                 this.messageDigest = getDefaultMessageDigest();
@@ -92,6 +95,9 @@
 
         /**
          * Sets the message digest.
+         * <p>
+         * The MD5 cryptographic algorithm is weak and should not be used.
+         * </p>
          *
          * @param messageDigest the message digest.
          */
@@ -101,6 +107,9 @@
 
         /**
          * Sets the name of the name of the message digest algorithm.
+         * <p>
+         * The MD5 cryptographic algorithm is weak and should not be used.
+         * </p>
          *
          * @param algorithm the name of the algorithm. See the MessageDigest section in the
          *                  <a href= "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest"> Java Cryptography
@@ -123,9 +132,10 @@
          * Constructs an MessageDigestMaintainingObserver for the given MessageDigest.
          *
          * @param messageDigest the message digest to use
+         * @throws NullPointerException if messageDigest is null.
          */
         public MessageDigestMaintainingObserver(final MessageDigest messageDigest) {
-            this.messageDigest = messageDigest;
+            this.messageDigest = Objects.requireNonNull(messageDigest, "messageDigest");
         }
 
         @Override
@@ -187,9 +197,13 @@
 
     /**
      * Constructs a new instance, which calculates a signature on the given stream, using the given {@link MessageDigest}.
+     * <p>
+     * The MD5 cryptographic algorithm is weak and should not be used.
+     * </p>
      *
      * @param inputStream   the stream to calculate the message digest for
      * @param messageDigest the message digest to use
+     * @throws NullPointerException if messageDigest is null.
      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
      */
     @Deprecated
@@ -200,6 +214,9 @@
 
     /**
      * Constructs a new instance, which calculates a signature on the given stream, using a {@link MessageDigest} with the given algorithm.
+     * <p>
+     * The MD5 cryptographic algorithm is weak and should not be used.
+     * </p>
      *
      * @param inputStream the stream to calculate the message digest for
      * @param algorithm   the name of the algorithm requested. See the MessageDigest section in the
diff --git a/src/main/java/org/apache/commons/io/input/MessageDigestInputStream.java b/src/main/java/org/apache/commons/io/input/MessageDigestInputStream.java
new file mode 100644
index 0000000..d822256
--- /dev/null
+++ b/src/main/java/org/apache/commons/io/input/MessageDigestInputStream.java
@@ -0,0 +1,193 @@
+/*
+ * 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 java.io.IOException;
+import java.io.InputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+
+import org.apache.commons.io.build.AbstractStreamBuilder;
+
+/**
+ * This class is an example for using an {@link ObservableInputStream}. It creates its own {@link org.apache.commons.io.input.ObservableInputStream.Observer},
+ * which calculates a checksum using a {@link MessageDigest}, for example, a SHA-512 sum.
+ * <p>
+ * To build an instance, see {@link Builder}.
+ * </p>
+ * <p>
+ * See the MessageDigest section in the <a href= "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest"> Java
+ * Cryptography Architecture Standard Algorithm Name Documentation</a> for information about standard algorithm names.
+ * </p>
+ * <p>
+ * You must specify a message digest algorithm name or instance.
+ * </p>
+ * <p>
+ * <em>Note</em>: Neither {@link ObservableInputStream}, nor {@link MessageDigest}, are thread safe, so is {@link MessageDigestInputStream}.
+ * </p>
+ *
+ * @since 2.15.0
+ */
+public final class MessageDigestInputStream extends ObservableInputStream {
+
+    /**
+     * Builds new {@link MessageDigestInputStream} instances.
+     * <p>
+     * For example:
+     * </p>
+     * <pre>{@code
+     * MessageDigestInputStream s = MessageDigestInputStream.builder()
+     *   .setPath(path)
+     *   .setMessageDigest("SHA-512")
+     *   .get();}
+     * </pre>
+     * <p>
+     * You must specify a message digest algorithm name or instance.
+     * </p>
+     */
+    public static class Builder extends AbstractStreamBuilder<MessageDigestInputStream, Builder> {
+
+        private MessageDigest messageDigest;
+
+        /**
+         * Constructs a new Builder.
+         */
+        public Builder() {
+            // empty
+        }
+
+        /**
+         * Constructs a new instance.
+         * <p>
+         * This builder use the aspects InputStream, OpenOption[], and MessageDigest.
+         * </p>
+         * <p>
+         * You must provide an origin that can be converted to an InputStream by this builder, otherwise, this call will throw an
+         * {@link UnsupportedOperationException}.
+         * </p>
+         *
+         * @return a new instance.
+         * @throws UnsupportedOperationException if the origin cannot provide an InputStream.
+         * @see #getInputStream()
+         */
+        @SuppressWarnings("resource")
+        @Override
+        public MessageDigestInputStream get() throws IOException {
+            return new MessageDigestInputStream(getInputStream(), messageDigest);
+        }
+
+        /**
+         * Sets the message digest.
+         * <p>
+         * The MD5 cryptographic algorithm is weak and should not be used.
+         * </p>
+         *
+         * @param messageDigest the message digest.
+         * @return this
+         */
+        public Builder setMessageDigest(final MessageDigest messageDigest) {
+            this.messageDigest = messageDigest;
+            return this;
+        }
+
+        /**
+         * Sets the name of the name of the message digest algorithm.
+         * <p>
+         * The MD5 cryptographic algorithm is weak and should not be used.
+         * </p>
+         *
+         * @param algorithm the name of the algorithm. See the MessageDigest section in the
+         *                  <a href= "https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest"> Java Cryptography
+         *                  Architecture Standard Algorithm Name Documentation</a> for information about standard algorithm names.
+         * @return this
+         * @throws NoSuchAlgorithmException if no Provider supports a MessageDigestSpi implementation for the specified algorithm.
+         */
+        public Builder setMessageDigest(final String algorithm) throws NoSuchAlgorithmException {
+            this.messageDigest = MessageDigest.getInstance(algorithm);
+            return this;
+        }
+
+    }
+
+    /**
+     * Maintains the message digest.
+     */
+    public static class MessageDigestMaintainingObserver extends Observer {
+
+        private final MessageDigest messageDigest;
+
+        /**
+         * Constructs an MessageDigestMaintainingObserver for the given MessageDigest.
+         *
+         * @param messageDigest the message digest to use
+         * @throws NullPointerException if messageDigest is null.
+         */
+        public MessageDigestMaintainingObserver(final MessageDigest messageDigest) {
+            this.messageDigest = Objects.requireNonNull(messageDigest, "messageDigest");
+        }
+
+        @Override
+        public void data(final byte[] input, final int offset, final int length) throws IOException {
+            messageDigest.update(input, offset, length);
+        }
+
+        @Override
+        public void data(final int input) throws IOException {
+            messageDigest.update((byte) input);
+        }
+    }
+
+    /**
+     * Constructs a new {@link Builder}.
+     *
+     * @return a new {@link Builder}.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private final MessageDigest messageDigest;
+
+    /**
+     * Constructs a new instance, which calculates a signature on the given stream, using the given {@link MessageDigest}.
+     * <p>
+     * The MD5 cryptographic algorithm is weak and should not be used.
+     * </p>
+     *
+     * @param inputStream   the stream to calculate the message digest for
+     * @param messageDigest the message digest to use
+     * @throws NullPointerException if messageDigest is null.
+     */
+    private MessageDigestInputStream(final InputStream inputStream, final MessageDigest messageDigest) {
+        super(inputStream, new MessageDigestMaintainingObserver(messageDigest));
+        this.messageDigest = messageDigest;
+    }
+
+    /**
+     * Gets the {@link MessageDigest}, which is being used for generating the checksum.
+     * <p>
+     * <em>Note</em>: The checksum will only reflect the data, which has been read so far. This is probably not, what you expect. Make sure, that the complete
+     * data has been read, if that is what you want. The easiest way to do so is by invoking {@link #consume()}.
+     * </p>
+     *
+     * @return the message digest used
+     */
+    public MessageDigest getMessageDigest() {
+        return messageDigest;
+    }
+}
diff --git a/src/main/java/org/apache/commons/io/input/ObservableInputStream.java b/src/main/java/org/apache/commons/io/input/ObservableInputStream.java
index ee080f0..a97702f 100644
--- a/src/main/java/org/apache/commons/io/input/ObservableInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/ObservableInputStream.java
@@ -39,7 +39,7 @@
  * be used.
  * </p>
  *
- * @see MessageDigestCalculatingInputStream
+ * @see MessageDigestInputStream
  */
 public class ObservableInputStream extends ProxyInputStream {
 
diff --git a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java
index 04c9cf9..7ef620f 100644
--- a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java
+++ b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java
@@ -66,6 +66,9 @@
      */
     public static class Builder extends AbstractStreamBuilder<ReversedLinesFileReader, Builder> {
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             setBufferSizeDefault(DEFAULT_BLOCK_SIZE);
             setBufferSize(DEFAULT_BLOCK_SIZE);
diff --git a/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java b/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java
index 31600f5..9239b52 100644
--- a/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java
@@ -29,7 +29,7 @@
  * DataInput for systems relying on little endian data formats. When read, values will be changed from little endian to
  * big endian formats for internal usage.
  * <p>
- * <b>Origin of code: </b>Avalon Excalibur (IO)
+ * Provenance: Avalon Excalibur (IO)
  * </p>
  */
 public class SwappedDataInputStream extends ProxyInputStream implements DataInput {
diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java
index ffd456d..b33a118 100644
--- a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java
@@ -105,7 +105,7 @@
     protected volatile byte[] buffer;
 
     /**
-     * The total number of bytes inside the byte array {@code buf}.
+     * The total number of bytes inside the byte array {@code buffer}.
      */
     protected int count;
 
@@ -120,7 +120,7 @@
     protected int markPos = IOUtils.EOF;
 
     /**
-     * The current position within the byte array {@code buf}.
+     * The current position within the byte array {@code buffer}.
      */
     protected int pos;
 
@@ -190,8 +190,8 @@
             }
             final byte[] newbuf = new byte[newLength];
             System.arraycopy(localBuf, 0, newbuf, 0, localBuf.length);
-            // Reassign buf, which will invalidate any local references
-            // FIXME: what if buf was null?
+            // Reassign buffer, which will invalidate any local references
+            // FIXME: what if buffer was null?
             localBuf = buffer = newbuf;
         } else if (markPos > 0) {
             System.arraycopy(localBuf, markPos, localBuf, 0, localBuf.length - markPos);
@@ -276,7 +276,7 @@
      * set and the requested number of bytes is larger than the receiver's buffer size, this implementation bypasses the buffer and simply places the results
      * directly into {@code buffer}.
      *
-     * @param buffer the byte array in which to store the bytes read.
+     * @param dest the byte array in which to store the bytes read.
      * @param offset the initial position in {@code buffer} to store the bytes read from this stream.
      * @param length the maximum number of bytes to store in {@code buffer}.
      * @return the number of bytes actually read or -1 if end of stream.
@@ -284,7 +284,7 @@
      * @throws IOException               if the stream is already closed or another IOException occurs.
      */
     @Override
-    public int read(final byte[] buffer, int offset, final int length) throws IOException {
+    public int read(final byte[] dest, int offset, final int length) throws IOException {
         // Use local ref since buf may be invalidated by an unsynchronized
         // close()
         byte[] localBuf = buffer;
@@ -292,7 +292,7 @@
             throw new IOException("Stream is closed");
         }
         // avoid int overflow
-        if (offset > buffer.length - length || offset < 0 || length < 0) {
+        if (offset > dest.length - length || offset < 0 || length < 0) {
             throw new IndexOutOfBoundsException();
         }
         if (length == 0) {
@@ -307,7 +307,7 @@
         if (pos < count) {
             /* There are bytes available in the buffer. */
             final int copylength = count - pos >= length ? length : count - pos;
-            System.arraycopy(localBuf, pos, buffer, offset, copylength);
+            System.arraycopy(localBuf, pos, dest, offset, copylength);
             pos += copylength;
             if (copylength == length || localIn.available() == 0) {
                 return copylength;
@@ -324,7 +324,7 @@
              * If we're not marked and the required size is greater than the buffer, simply read the bytes directly bypassing the buffer.
              */
             if (markPos == IOUtils.EOF && required >= localBuf.length) {
-                read = localIn.read(buffer, offset, required);
+                read = localIn.read(dest, offset, required);
                 if (read == IOUtils.EOF) {
                     return required == length ? IOUtils.EOF : length - required;
                 }
@@ -341,7 +341,7 @@
                 }
 
                 read = count - pos >= required ? required : count - pos;
-                System.arraycopy(localBuf, pos, buffer, offset, read);
+                System.arraycopy(localBuf, pos, dest, offset, read);
                 pos += read;
             }
             required -= read;
@@ -397,10 +397,12 @@
         }
 
         if (count - pos >= amount) {
-            pos += amount;
+            // (int count - int pos) here is always an int so amount is also in the int range if the above test is true.
+            // We can safely cast to int and avoid static analysis warnings.
+            pos += (int) amount;
             return amount;
         }
-        long read = count - pos;
+        int read = count - pos;
         pos = count;
 
         if (markPos != IOUtils.EOF && amount <= markLimit) {
@@ -408,7 +410,9 @@
                 return read;
             }
             if (count - pos >= amount - read) {
-                pos += amount - read;
+                // (int count - int pos) here is always an int so (amount - read) is also in the int range if the above test is true.
+                // We can safely cast to int and avoid static analysis warnings.
+                pos += (int) amount - read;
                 return amount;
             }
             // Couldn't get all the bytes, skip what we read
diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java
index a4fc696..f10c112 100644
--- a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java
+++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java
@@ -106,6 +106,12 @@
             return super.setByteArray(origin);
         }
 
+        /**
+         * Sets the length.
+         *
+         * @param length Must be greater or equal to 0.
+         * @return this.
+         */
         public Builder setLength(final int length) {
             if (length < 0) {
                 throw new IllegalArgumentException("length cannot be negative");
@@ -114,6 +120,12 @@
             return this;
         }
 
+        /**
+         * Sets the offset.
+         *
+         * @param offset Must be greater or equal to 0.
+         * @return this.
+         */
         public Builder setOffset(final int offset) {
             if (offset < 0) {
                 throw new IllegalArgumentException("offset cannot be negative");
diff --git a/src/main/java/org/apache/commons/io/input/XmlStreamReader.java b/src/main/java/org/apache/commons/io/input/XmlStreamReader.java
index a38ef80..290e536 100644
--- a/src/main/java/org/apache/commons/io/input/XmlStreamReader.java
+++ b/src/main/java/org/apache/commons/io/input/XmlStreamReader.java
@@ -157,11 +157,23 @@
             return super.setCharset(Charsets.toCharset(charset, getCharsetDefault()));
         }
 
+        /**
+         * Sets the HTTP content type.
+         *
+         * @param httpContentType the HTTP content type.
+         * @return this.
+         */
         public Builder setHttpContentType(final String httpContentType) {
             this.httpContentType = httpContentType;
             return this;
         }
 
+        /**
+         * Sets the lenient toggle.
+         *
+         * @param lenient the lenient toggle.
+         * @return this.
+         */
         public Builder setLenient(final boolean lenient) {
             this.lenient = lenient;
             return this;
@@ -209,8 +221,14 @@
     // @formatter:off
             "^<\\?xml\\s+"
             + "version\\s*=\\s*(?:(?:\"1\\.[0-9]+\")|(?:'1.[0-9]+'))\\s+"
-            + "encoding\\s*=\\s*((?:\"[A-Za-z]([A-Za-z0-9\\._]|-)*\")|(?:'[A-Za-z]([A-Za-z0-9\\\\._]|-)*'))",
+            + "encoding\\s*=\\s*"
+            + "((?:\"[A-Za-z0-9][A-Za-z0-9._+:-]*\")"  // double-quoted
+            +  "|(?:'[A-Za-z0-9][A-Za-z0-9._+:-]*'))", // single-quoted
             Pattern.MULTILINE);
+    // N.B. the documented pattern is
+    // EncName   ::=   [A-Za-z] ([A-Za-z0-9._] | '-')*
+    // However this does not match all the aliases that are supported by Java.
+    // e.g.  '437', 'ISO_8859-1:1987' and 'ebcdic-de-273+euro'
     // @formatter:on
 
     private static final String RAW_EX_1 = "Illegal encoding, BOM [{0}] XML guess [{1}] XML prolog [{2}] encoding mismatch";
diff --git a/src/main/java/org/apache/commons/io/output/DeferredFileOutputStream.java b/src/main/java/org/apache/commons/io/output/DeferredFileOutputStream.java
index d4dc19e..3382bdc 100644
--- a/src/main/java/org/apache/commons/io/output/DeferredFileOutputStream.java
+++ b/src/main/java/org/apache/commons/io/output/DeferredFileOutputStream.java
@@ -70,6 +70,9 @@
         private String suffix;
         private Path directory;
 
+        /**
+         * Constructs a new builder.
+         */
         public Builder() {
             setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
             setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
@@ -265,28 +268,6 @@
     }
 
     /**
-     * Constructs an instance of this class which will trigger an event at the specified threshold, and save data either to a file beyond that point.
-     *
-     * @param threshold         The number of bytes at which to trigger an event.
-     * @param outputFile        The file to which data is saved beyond the threshold.
-     * @param prefix            Prefix to use for the temporary file.
-     * @param suffix            Suffix to use for the temporary file.
-     * @param directory         Temporary file directory.
-     * @param initialBufferSize The initial size of the in memory buffer.
-     * @throws IllegalArgumentException if initialBufferSize &lt; 0.
-     */
-    private DeferredFileOutputStream(final int threshold, final Path outputFile, final String prefix, final String suffix, final Path directory,
-            final int initialBufferSize) {
-        super(threshold);
-        this.outputPath = toPath(outputFile, null);
-        this.prefix = prefix;
-        this.suffix = suffix;
-        this.directory = toPath(directory, PathUtils::getTempDirectory);
-        this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(initialBufferSize));
-        this.currentOutputStream = memoryOutputStream;
-    }
-
-    /**
      * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a file beyond that point.
      *
      * @param threshold         The number of bytes at which to trigger an event.
@@ -317,6 +298,28 @@
     }
 
     /**
+     * Constructs an instance of this class which will trigger an event at the specified threshold, and save data either to a file beyond that point.
+     *
+     * @param threshold         The number of bytes at which to trigger an event.
+     * @param outputFile        The file to which data is saved beyond the threshold.
+     * @param prefix            Prefix to use for the temporary file.
+     * @param suffix            Suffix to use for the temporary file.
+     * @param directory         Temporary file directory.
+     * @param initialBufferSize The initial size of the in memory buffer.
+     * @throws IllegalArgumentException if initialBufferSize &lt; 0.
+     */
+    private DeferredFileOutputStream(final int threshold, final Path outputFile, final String prefix, final String suffix, final Path directory,
+            final int initialBufferSize) {
+        super(threshold);
+        this.outputPath = toPath(outputFile, null);
+        this.prefix = prefix;
+        this.suffix = suffix;
+        this.directory = toPath(directory, PathUtils::getTempDirectory);
+        this.memoryOutputStream = new ByteArrayOutputStream(checkBufferSize(initialBufferSize));
+        this.currentOutputStream = memoryOutputStream;
+    }
+
+    /**
      * Constructs an instance of this class which will trigger an event at the specified threshold, and save data to a temporary file beyond that point. The
      * initial buffer size will default to 32 bytes which is ByteArrayOutputStream's default buffer size.
      *
diff --git a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java
index 064de22..b33abc8 100644
--- a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java
+++ b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java
@@ -70,6 +70,9 @@
         private boolean append;
         private AbstractOrigin<?, ?> lockDirectory = AbstractOriginSupplier.newFileOrigin(FileUtils.getTempDirectoryPath());
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             setBufferSizeDefault(AbstractByteArrayOutputStream.DEFAULT_SIZE);
             setBufferSize(AbstractByteArrayOutputStream.DEFAULT_SIZE);
diff --git a/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java b/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java
index 1d8a3a0..8102eb5 100644
--- a/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java
+++ b/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java
@@ -149,11 +149,10 @@
      *
      * @return The underlying output stream.
      * @throws IOException if an error occurs.
-     * @deprecated Use {@link #getOutputStream()}.
+     * @since 2.14.0
      */
-    @Deprecated
-    protected OutputStream getStream() throws IOException {
-        return getOutputStream();
+    protected OutputStream getOutputStream() throws IOException {
+        return outputStreamGetter.apply(this);
     }
 
     /**
@@ -162,10 +161,11 @@
      *
      * @return The underlying output stream.
      * @throws IOException if an error occurs.
-     * @since 2.14.0
+     * @deprecated Use {@link #getOutputStream()}.
      */
-    protected OutputStream getOutputStream() throws IOException {
-        return outputStreamGetter.apply(this);
+    @Deprecated
+    protected OutputStream getStream() throws IOException {
+        return getOutputStream();
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/io/output/WriterOutputStream.java b/src/main/java/org/apache/commons/io/output/WriterOutputStream.java
index dc4cd6f..bc2c5c2 100644
--- a/src/main/java/org/apache/commons/io/output/WriterOutputStream.java
+++ b/src/main/java/org/apache/commons/io/output/WriterOutputStream.java
@@ -99,6 +99,9 @@
         private CharsetDecoder charsetDecoder;
         private boolean writeImmediately;
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             this.charsetDecoder = getCharset().newDecoder();
         }
diff --git a/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java b/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java
index b40a2ac..6f732a7 100644
--- a/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java
+++ b/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java
@@ -62,6 +62,9 @@
      */
     public static class Builder extends AbstractStreamBuilder<XmlStreamWriter, Builder> {
 
+        /**
+         * Constructs a new Builder.
+         */
         public Builder() {
             setCharsetDefault(StandardCharsets.UTF_8);
             setCharset(StandardCharsets.UTF_8);
diff --git a/src/main/java/org/apache/commons/io/package-info.java b/src/main/java/org/apache/commons/io/package-info.java
index 29464fe..6633c10 100644
--- a/src/main/java/org/apache/commons/io/package-info.java
+++ b/src/main/java/org/apache/commons/io/package-info.java
@@ -24,7 +24,7 @@
  * <b>FileUtils</b> provides operations based around the JDK File class. These include reading, writing, copying, comparing and deleting.
  * </p>
  * <p>
- * <b>FilenameUtils</b> provides utilities based on filenames. This utility class manipulates filenames without using File objects. It aims to simplify the
+ * <b>FilenameUtils</b> provides utilities based on filenames. This utility class manipulates file names without using File objects. It aims to simplify the
  * transition between Windows and Unix. Before using this class however, you should consider whether you should be using File objects.
  * </p>
  * <p>
diff --git a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java
index bde163c..6e60f32 100644
--- a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java
+++ b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java
@@ -110,6 +110,32 @@
     }
 
     /**
+     * Checks that the class name conforms to requirements.
+     *
+     * @param name The class name
+     * @throws InvalidClassException when a non-accepted class is encountered
+     */
+    private void checkClassName(final String name) throws InvalidClassException {
+        // Reject has precedence over accept
+        for (final ClassNameMatcher m : rejectMatchers) {
+            if (m.matches(name)) {
+                invalidClassNameFound(name);
+            }
+        }
+
+        boolean ok = false;
+        for (final ClassNameMatcher m : acceptMatchers) {
+            if (m.matches(name)) {
+                ok = true;
+                break;
+            }
+        }
+        if (!ok) {
+            invalidClassNameFound(name);
+        }
+    }
+
+    /**
      * Called to throw {@link InvalidClassException} if an invalid
      * class name is found during deserialization. Can be overridden, for example
      * to log those class names.
@@ -172,31 +198,7 @@
 
     @Override
     protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
-        validateClassName(osc.getName());
+        checkClassName(osc.getName());
         return super.resolveClass(osc);
     }
-
-    /** Check that the classname conforms to requirements.
-     * @param name The class name
-     * @throws InvalidClassException when a non-accepted class is encountered
-     */
-    private void validateClassName(final String name) throws InvalidClassException {
-        // Reject has precedence over accept
-        for (final ClassNameMatcher m : rejectMatchers) {
-            if (m.matches(name)) {
-                invalidClassNameFound(name);
-            }
-        }
-
-        boolean ok = false;
-        for (final ClassNameMatcher m : acceptMatchers) {
-            if (m.matches(name)) {
-                ok = true;
-                break;
-            }
-        }
-        if (!ok) {
-            invalidClassNameFound(name);
-        }
-    }
 }
\ No newline at end of file
diff --git a/src/site/xdoc/download_io.xml b/src/site/xdoc/download_io.xml
index 1d7ef8e..02445fe 100644
--- a/src/site/xdoc/download_io.xml
+++ b/src/site/xdoc/download_io.xml
@@ -113,32 +113,32 @@
       </p>
     </subsection>
     </section>
-    <section name="Apache Commons IO 2.14.0 (requires Java 8)">
+    <section name="Apache Commons IO 2.15.0 (requires Java 8)">
       <subsection name="Binaries">
         <table>
           <tr>
-              <td><a href="[preferred]/commons/io/binaries/commons-io-2.14.0-bin.tar.gz">commons-io-2.14.0-bin.tar.gz</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.14.0-bin.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.14.0-bin.tar.gz.asc">pgp</a></td>
+              <td><a href="[preferred]/commons/io/binaries/commons-io-2.15.0-bin.tar.gz">commons-io-2.15.0-bin.tar.gz</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.15.0-bin.tar.gz.sha512">sha512</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.15.0-bin.tar.gz.asc">pgp</a></td>
           </tr>
           <tr>
-              <td><a href="[preferred]/commons/io/binaries/commons-io-2.14.0-bin.zip">commons-io-2.14.0-bin.zip</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.14.0-bin.zip.sha512">sha512</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.14.0-bin.zip.asc">pgp</a></td>
+              <td><a href="[preferred]/commons/io/binaries/commons-io-2.15.0-bin.zip">commons-io-2.15.0-bin.zip</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.15.0-bin.zip.sha512">sha512</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/binaries/commons-io-2.15.0-bin.zip.asc">pgp</a></td>
           </tr>
         </table>
       </subsection>
       <subsection name="Source">
         <table>
           <tr>
-              <td><a href="[preferred]/commons/io/source/commons-io-2.14.0-src.tar.gz">commons-io-2.14.0-src.tar.gz</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.14.0-src.tar.gz.sha512">sha512</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.14.0-src.tar.gz.asc">pgp</a></td>
+              <td><a href="[preferred]/commons/io/source/commons-io-2.15.0-src.tar.gz">commons-io-2.15.0-src.tar.gz</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.15.0-src.tar.gz.sha512">sha512</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.15.0-src.tar.gz.asc">pgp</a></td>
           </tr>
           <tr>
-              <td><a href="[preferred]/commons/io/source/commons-io-2.14.0-src.zip">commons-io-2.14.0-src.zip</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.14.0-src.zip.sha512">sha512</a></td>
-              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.14.0-src.zip.asc">pgp</a></td>
+              <td><a href="[preferred]/commons/io/source/commons-io-2.15.0-src.zip">commons-io-2.15.0-src.zip</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.15.0-src.zip.sha512">sha512</a></td>
+              <td><a href="https://downloads.apache.org/commons/io/source/commons-io-2.15.0-src.zip.asc">pgp</a></td>
           </tr>
         </table>
       </subsection>
diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml
index 1723036..faecfc2 100644
--- a/src/site/xdoc/index.xml
+++ b/src/site/xdoc/index.xml
@@ -118,18 +118,9 @@
           The Java platform requirements are:
         </p>
         <ul>
-          <li>Version 2.14.0 requires Java 8.</li>
-          <li>Version 2.13.0 requires Java 8.</li>
-          <li>Version 2.12.0 requires Java 8.</li>
-          <li>Version 2.11.0 requires Java 8.</li>
-          <li>Version 2.10.0 requires Java 8.</li>
-          <li>Version 2.9.0 requires Java 8.</li>
-          <li>Version 2.8.0 requires Java 8.</li>
-          <li>Version 2.7 requires Java 8.</li>
+          <li>Version 2.7 and up requires Java 8.</li>
           <li>Version 2.6 requires Java 7.</li>
-          <li>Version 2.5 requires Java 6.</li>
-          <li>Version 2.4 requires Java 6.</li>
-          <li>Version 2.3 requires Java 6.</li>
+          <li>Version 2.3 through 2.5 requires Java 6.</li>
           <li>Version 2.2 requires Java 5.</li>
         </ul>
       </subsection>
diff --git a/src/site/xdoc/mail-lists.xml b/src/site/xdoc/mail-lists.xml
index 36189d5..bb94ded 100644
--- a/src/site/xdoc/mail-lists.xml
+++ b/src/site/xdoc/mail-lists.xml
@@ -59,9 +59,9 @@
       </p>
       <p>
         Questions related to the usage of Apache Commons IO should be posted to the
-        <a href="https://mail-archives.apache.org/mod_mbox/commons-user/">User List</a>.
+        <a href="https://lists.apache.org/list.html?user@commons.apache.org">User List</a>.
         <br />
-        The <a href="https://mail-archives.apache.org/mod_mbox/commons-dev/">Developer List</a>
+        The <a href="https://lists.apache.org/list.html?dev@commons.apache.org">Developer List</a>
         is for questions and discussion related to the development of Apache Commons IO.
         <br />
         Please do not cross-post; developers are also subscribed to the user list.
@@ -70,8 +70,10 @@
         to subscribe.
       </p>
       <p>
-        <strong>Note:</strong> please don't send patches or attachments to any of the mailing lists.
+        <strong>Note:</strong> please don't send patches or attachments to any of the mailing lists;
+        most of the lists are set up to drop attachments.
         Patches are best handled via the <a href="issue-tracking.html">Issue Tracking</a> system.
+        If you have a GitHub account, most components also accept PRs (pull requests).
         Otherwise, please upload the file to a public server and include the URL in the mail.
       </p>
     </section>
@@ -105,12 +107,11 @@
           <td><a href="mailto:user-subscribe@commons.apache.org">Subscribe</a></td>
           <td><a href="mailto:user-unsubscribe@commons.apache.org">Unsubscribe</a></td>
           <td><a href="mailto:user@commons.apache.org?subject=[io]">Post</a></td>
-          <td><a href="https://mail-archives.apache.org/mod_mbox/commons-user/">mail-archives.apache.org</a><br />
+          <td>
               <a href="https://lists.apache.org/list.html?user@commons.apache.org">lists.apache.org</a>
           </td>
-          <td><a href="https://markmail.org/list/org.apache.commons.users/">markmail.org</a><br />
-              <a href="https://www.mail-archive.com/user@commons.apache.org/">www.mail-archive.com</a><br />
-              <a href="https://news.gmane.org/gmane.comp.jakarta.commons.devel">news.gmane.org</a>
+          <td>
+              <a href="https://www.mail-archive.com/user@commons.apache.org/">www.mail-archive.com</a>
           </td>
         </tr>
 
@@ -125,12 +126,11 @@
           <td><a href="mailto:dev-subscribe@commons.apache.org">Subscribe</a></td>
           <td><a href="mailto:dev-unsubscribe@commons.apache.org">Unsubscribe</a></td>
           <td><a href="mailto:dev@commons.apache.org?subject=[io]">Post</a></td>
-          <td><a href="https://mail-archives.apache.org/mod_mbox/commons-dev/">mail-archives.apache.org</a><br />
+          <td>
               <a href="https://lists.apache.org/list.html?dev@commons.apache.org">lists.apache.org</a>
           </td>
-          <td><a href="https://markmail.org/list/org.apache.commons.dev/">markmail.org</a><br />
-              <a href="https://www.mail-archive.com/dev@commons.apache.org/">www.mail-archive.com</a><br />
-              <a href="https://news.gmane.org/gmane.comp.jakarta.commons.devel">news.gmane.org</a>
+          <td>
+              <a href="https://www.mail-archive.com/dev@commons.apache.org/">www.mail-archive.com</a>
           </td>
         </tr>
 
@@ -145,10 +145,10 @@
           <td><a href="mailto:issues-subscribe@commons.apache.org">Subscribe</a></td>
           <td><a href="mailto:issues-unsubscribe@commons.apache.org">Unsubscribe</a></td>
           <td><i>read only</i></td>
-          <td><a href="https://mail-archives.apache.org/mod_mbox/commons-issues/">mail-archives.apache.org</a><br />
+          <td>
               <a href="https://lists.apache.org/list.html?issues@commons.apache.org">lists.apache.org</a>
           </td>
-          <td><a href="https://markmail.org/list/org.apache.commons.issues/">markmail.org</a><br />
+          <td>
               <a href="https://www.mail-archive.com/issues@commons.apache.org/">www.mail-archive.com</a>
           </td>
         </tr>
@@ -164,10 +164,10 @@
           <td><a href="mailto:commits-subscribe@commons.apache.org">Subscribe</a></td>
           <td><a href="mailto:commits-unsubscribe@commons.apache.org">Unsubscribe</a></td>
           <td><i>read only</i></td>
-          <td><a href="https://mail-archives.apache.org/mod_mbox/commons-commits/">mail-archives.apache.org</a><br />
+          <td>
               <a href="https://lists.apache.org/list.html?commits@commons.apache.org">lists.apache.org</a>
           </td>
-          <td><a href="https://markmail.org/list/org.apache.commons.commits/">markmail.org</a><br />
+          <td>
               <a href="https://www.mail-archive.com/commits@commons.apache.org/">www.mail-archive.com</a>
           </td>
         </tr>
@@ -199,13 +199,11 @@
           <td><a class="externalLink" href="mailto:announce-subscribe@apache.org">Subscribe</a></td>
           <td><a class="externalLink" href="mailto:announce-unsubscribe@apache.org">Unsubscribe</a></td>
           <td><i>read only</i></td>
-          <td><a class="externalLink" href="https://mail-archives.apache.org/mod_mbox/www-announce/">mail-archives.apache.org</a><br />
+          <td>
               <a class="externalLink" href="https://lists.apache.org/list.html?announce@apache.org">lists.apache.org</a>
           </td>
-          <td><a class="externalLink" href="https://markmail.org/list/org.apache.announce/">markmail.org</a><br />
-              <a class="externalLink" href="https://old.nabble.com/Apache-News-and-Announce-f109.html">old.nabble.com</a><br />
-              <a class="externalLink" href="https://www.mail-archive.com/announce@apache.org/">www.mail-archive.com</a><br />
-              <a class="externalLink" href="https://news.gmane.org/gmane.comp.apache.announce">news.gmane.org</a>
+          <td>
+              <a class="externalLink" href="https://www.mail-archive.com/announce@apache.org/">www.mail-archive.com</a>
           </td>
         </tr>
       </table>
diff --git a/src/test/java/org/apache/commons/io/CharsetsTest.java b/src/test/java/org/apache/commons/io/CharsetsTest.java
index b68b90b..7c98897 100644
--- a/src/test/java/org/apache/commons/io/CharsetsTest.java
+++ b/src/test/java/org/apache/commons/io/CharsetsTest.java
@@ -21,6 +21,7 @@
 
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.Set;
 import java.util.SortedMap;
 
 import org.junit.jupiter.api.Test;
@@ -31,6 +32,20 @@
 @SuppressWarnings("deprecation") // testing deprecated code
 public class CharsetsTest {
 
+    /**
+     * For parameterized tests.
+     */
+    public static final String AVAIL_CHARSETS = "org.apache.commons.io.CharsetsTest#availableCharsetsKeySet";
+
+    /**
+     * For parameterized tests.
+     *
+     * @return {@code Charset.availableCharsets().keySet()}.
+     */
+    public static Set<String> availableCharsetsKeySet() {
+        return Charset.availableCharsets().keySet();
+    }
+
     @Test
     public void testIso8859_1() {
         assertEquals("ISO-8859-1", Charsets.ISO_8859_1.name());
diff --git a/src/test/java/org/apache/commons/io/DemuxInputStreamTest.java b/src/test/java/org/apache/commons/io/DemuxInputStreamTest.java
index 6872bc5..6b1a63b 100644
--- a/src/test/java/org/apache/commons/io/DemuxInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/DemuxInputStreamTest.java
@@ -37,7 +37,7 @@
  */
 public class DemuxInputStreamTest {
 
-    private static class ReaderThread extends Thread {
+    private static final class ReaderThread extends Thread {
         private final DemuxInputStream demuxInputStream;
         private final InputStream inputStream;
         private final StringBuffer stringBuffer = new StringBuffer();
@@ -62,7 +62,7 @@
                     // System.out.println( "Reading: " + (char)ch );
                     stringBuffer.append((char) ch);
 
-                    final int sleepMillis = Math.abs(c_random.nextInt() % 10);
+                    final int sleepMillis = Math.abs(RANDOM.nextInt() % 10);
                     TestUtils.sleep(sleepMillis);
                     ch = demuxInputStream.read();
                 }
@@ -72,7 +72,7 @@
         }
     }
 
-    private static class WriterThread extends Thread {
+    private static final class WriterThread extends Thread {
         private final byte[] byteArray;
         private final DemuxOutputStream demuxOutputStream;
         private final OutputStream outputStream;
@@ -91,7 +91,7 @@
                 try {
                     // System.out.println( "Writing: " + (char)byteArray[ i ] );
                     demuxOutputStream.write(element);
-                    final int sleepMillis = Math.abs(c_random.nextInt() % 10);
+                    final int sleepMillis = Math.abs(RANDOM.nextInt() % 10);
                     TestUtils.sleep(sleepMillis);
                 } catch (final Exception e) {
                     e.printStackTrace();
@@ -100,7 +100,7 @@
         }
     }
 
-    private static final Random c_random = new Random();
+    private static final Random RANDOM = new Random();
     private static final String DATA1 = "Data for thread1";
 
     private static final String DATA2 = "Data for thread2";
diff --git a/src/test/java/org/apache/commons/io/DirectoryWalkerTest.java b/src/test/java/org/apache/commons/io/DirectoryWalkerTest.java
index 5ac3aa9..ccb5c11 100644
--- a/src/test/java/org/apache/commons/io/DirectoryWalkerTest.java
+++ b/src/test/java/org/apache/commons/io/DirectoryWalkerTest.java
@@ -90,7 +90,7 @@
      * Test DirectoryWalker implementation that always returns false
      * from handleDirectoryStart()
      */
-    private static class TestFalseFileFinder extends TestFileFinder {
+    private static final class TestFalseFileFinder extends TestFileFinder {
 
         protected TestFalseFileFinder(final FileFilter filter, final int depthLimit) {
             super(filter, depthLimit);
@@ -143,7 +143,7 @@
      * Test DirectoryWalker implementation that finds files in a directory hierarchy
      * applying a file filter.
      */
-    private static class TestFileFinderString extends DirectoryWalker<String> {
+    private static final class TestFileFinderString extends DirectoryWalker<String> {
 
         protected TestFileFinderString(final FileFilter filter, final int depthLimit) {
             super(filter, depthLimit);
@@ -234,12 +234,12 @@
     private static final File outputDir    = new File(ioDir, "output");
     private static final File[] dirs       = {orgDir, apacheDir, commonsDir, ioDir, outputDir};
     // Files
-    private static final File filenameUtils = new File(ioDir, "FilenameUtils.java");
+    private static final File fileNameUtils = new File(ioDir, "FilenameUtils.java");
 
     private static final File ioUtils       = new File(ioDir, "IOUtils.java");
     private static final File proxyWriter   = new File(outputDir, "ProxyWriter.java");
     private static final File nullStream    = new File(outputDir, "NullOutputStream.java");
-    private static final File[] ioFiles     = {filenameUtils, ioUtils};
+    private static final File[] ioFiles     = {fileNameUtils, ioUtils};
     private static final File[] outputFiles = {proxyWriter, nullStream};
 
     // Filters
diff --git a/src/test/java/org/apache/commons/io/DirectoryWalkerTestCaseJava4.java b/src/test/java/org/apache/commons/io/DirectoryWalkerTestCaseJava4.java
index f55699a..ad7337d 100644
--- a/src/test/java/org/apache/commons/io/DirectoryWalkerTestCaseJava4.java
+++ b/src/test/java/org/apache/commons/io/DirectoryWalkerTestCaseJava4.java
@@ -102,7 +102,7 @@
      * Test DirectoryWalker implementation that always returns false
      * from handleDirectoryStart()
      */
-    private static class TestFalseFileFinder extends TestFileFinder {
+    private static final class TestFalseFileFinder extends TestFileFinder {
 
         protected TestFalseFileFinder(final FileFilter filter, final int depthLimit) {
             super(filter, depthLimit);
@@ -237,12 +237,12 @@
     private static final File outputDir = new File(ioDir, "output");
     private static final File[] dirs = {orgDir, apacheDir, commonsDir, ioDir, outputDir};
     // Files
-    private static final File filenameUtils = new File(ioDir, "FilenameUtils.java");
+    private static final File fileNameUtils = new File(ioDir, "FilenameUtils.java");
     private static final File ioUtils = new File(ioDir, "IOUtils.java");
 
     private static final File proxyWriter = new File(outputDir, "ProxyWriter.java");
     private static final File nullStream = new File(outputDir, "NullOutputStream.java");
-    private static final File[] ioFiles = {filenameUtils, ioUtils};
+    private static final File[] ioFiles = {fileNameUtils, ioUtils};
     private static final File[] outputFiles = {proxyWriter, nullStream};
     // Filters
     private static final IOFileFilter dirsFilter = createNameFilter(dirs);
diff --git a/src/test/java/org/apache/commons/io/EndianUtilsTest.java b/src/test/java/org/apache/commons/io/EndianUtilsTest.java
index 79df0c6..4247644 100644
--- a/src/test/java/org/apache/commons/io/EndianUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/EndianUtilsTest.java
@@ -38,7 +38,7 @@
     }
 
     @Test
-    public void testEOFException() throws IOException {
+    public void testEOFException() {
         final ByteArrayInputStream input = new ByteArrayInputStream(new byte[] {});
         assertThrows(EOFException.class, () -> EndianUtils.readSwappedDouble(input));
     }
diff --git a/src/test/java/org/apache/commons/io/FileUtilsDirectoryContainsTest.java b/src/test/java/org/apache/commons/io/FileUtilsDirectoryContainsTest.java
index bba9f09..3e92fef 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsDirectoryContainsTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsDirectoryContainsTest.java
@@ -112,7 +112,7 @@
     }
 
     @Test
-    public void testDirectoryDoesNotExist() throws IOException {
+    public void testDirectoryDoesNotExist() {
         final File dir = new File("DOESNOTEXIST");
         assertFalse(dir.exists());
         assertThrows(IllegalArgumentException.class, () -> FileUtils.directoryContains(dir, file1));
@@ -158,12 +158,12 @@
     }
 
     @Test
-    public void testSameFile() throws IOException {
+    public void testSameFile() {
         assertThrows(IllegalArgumentException.class, () -> FileUtils.directoryContains(file1, file1));
     }
 
     @Test
-    public void testUnrealizedContainment() throws IOException {
+    public void testUnrealizedContainment() {
         final File dir = new File("DOESNOTEXIST");
         final File file = new File(dir, "DOESNOTEXIST2");
         assertFalse(dir.exists());
diff --git a/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java b/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java
index 27a81c9..e65bc8d 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java
@@ -29,6 +29,7 @@
 
 import org.apache.commons.io.filefilter.FileFilterUtils;
 import org.apache.commons.io.filefilter.IOFileFilter;
+import org.apache.commons.lang3.function.Consumers;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -45,8 +46,15 @@
         return files.stream().map(File::getName).collect(Collectors.toList());
     }
 
+    /**
+     * Consumes and closes the underlying stream.
+     *
+     * @param files The iterator to consume.
+     * @return a new collection.
+     */
     private Collection<String> filesToFilenames(final Iterator<File> files) {
         final Collection<String> fileNames = new ArrayList<>();
+        // Iterator.forEachRemaining() closes the underlying stream.
         files.forEachRemaining(f -> fileNames.add(f.getName()));
         return fileNames;
     }
@@ -88,68 +96,83 @@
         final String[] extensions = { "xml", "txt" };
 
         Iterator<File> files = FileUtils.iterateFiles(temporaryFolder, extensions, false);
-        Collection<String> filenames = filesToFilenames(files);
-        assertEquals(1, filenames.size());
-        assertTrue(filenames.contains("dummy-build.xml"));
-        assertFalse(filenames.contains("README"));
-        assertFalse(filenames.contains("dummy-file.txt"));
+        try {
+            final Collection<String> fileNames = filesToFilenames(files);
+            assertEquals(1, fileNames.size());
+            assertTrue(fileNames.contains("dummy-build.xml"));
+            assertFalse(fileNames.contains("README"));
+            assertFalse(fileNames.contains("dummy-file.txt"));
+        } finally {
+            // Backstop in case filesToFilenames() failure.
+            files.forEachRemaining(Consumers.nop());
+        }
 
-        files = FileUtils.iterateFiles(temporaryFolder, extensions, true);
-        filenames = filesToFilenames(files);
-        assertEquals(4, filenames.size());
-        assertTrue(filenames.contains("dummy-file.txt"));
-        assertFalse(filenames.contains("dummy-index.html"));
+        try {
+            files = FileUtils.iterateFiles(temporaryFolder, extensions, true);
+            final Collection<String> fileNames = filesToFilenames(files);
+            assertEquals(4, fileNames.size());
+            assertTrue(fileNames.contains("dummy-file.txt"));
+            assertFalse(fileNames.contains("dummy-index.html"));
+        } finally {
+            // Backstop in case filesToFilenames() failure.
+            files.forEachRemaining(Consumers.nop());
+        }
 
         files = FileUtils.iterateFiles(temporaryFolder, null, false);
-        filenames = filesToFilenames(files);
-        assertEquals(2, filenames.size());
-        assertTrue(filenames.contains("dummy-build.xml"));
-        assertTrue(filenames.contains("README"));
-        assertFalse(filenames.contains("dummy-file.txt"));
+        try {
+            final Collection<String> fileNames = filesToFilenames(files);
+            assertEquals(2, fileNames.size());
+            assertTrue(fileNames.contains("dummy-build.xml"));
+            assertTrue(fileNames.contains("README"));
+            assertFalse(fileNames.contains("dummy-file.txt"));
+        } finally {
+            // Backstop in case filesToFilenames() failure.
+            files.forEachRemaining(Consumers.nop());
+        }
     }
 
     @Test
     public void testListFiles() {
         Collection<File> files;
-        Collection<String> filenames;
+        Collection<String> fileNames;
         IOFileFilter fileFilter;
         IOFileFilter dirFilter;
 
         // First, find non-recursively
         fileFilter = FileFilterUtils.trueFileFilter();
         files = FileUtils.listFiles(temporaryFolder, fileFilter, null);
-        filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
-        assertFalse(filenames.contains("dummy-index.html"), "'dummy-index.html' shouldn't be found");
-        assertFalse(filenames.contains("Entries"), "'Entries' shouldn't be found");
+        fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
+        assertFalse(fileNames.contains("dummy-index.html"), "'dummy-index.html' shouldn't be found");
+        assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
 
         // Second, find recursively
         fileFilter = FileFilterUtils.trueFileFilter();
         dirFilter = FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter("CVS"));
         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
-        filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
-        assertTrue(filenames.contains("dummy-index.html"), "'dummy-index.html' is missing");
-        assertFalse(filenames.contains("Entries"), "'Entries' shouldn't be found");
+        fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
+        assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
+        assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
 
         // Do the same as above but now with the filter coming from FileFilterUtils
         fileFilter = FileFilterUtils.trueFileFilter();
         dirFilter = FileFilterUtils.makeCVSAware(null);
         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
-        filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
-        assertTrue(filenames.contains("dummy-index.html"), "'dummy-index.html' is missing");
-        assertFalse(filenames.contains("Entries"), "'Entries' shouldn't be found");
+        fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
+        assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
+        assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
 
         // Again with the CVS filter but now with a non-null parameter
         fileFilter = FileFilterUtils.trueFileFilter();
         dirFilter = FileFilterUtils.prefixFileFilter("sub");
         dirFilter = FileFilterUtils.makeCVSAware(dirFilter);
         files = FileUtils.listFiles(temporaryFolder, fileFilter, dirFilter);
-        filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
-        assertTrue(filenames.contains("dummy-index.html"), "'dummy-index.html' is missing");
-        assertFalse(filenames.contains("Entries"), "'Entries' shouldn't be found");
+        fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"), "'dummy-build.xml' is missing");
+        assertTrue(fileNames.contains("dummy-index.html"), "'dummy-index.html' is missing");
+        assertFalse(fileNames.contains("Entries"), "'Entries' shouldn't be found");
 
         assertThrows(NullPointerException.class, () -> FileUtils.listFiles(temporaryFolder, null, null));
     }
@@ -160,23 +183,23 @@
 
         Collection<File> files = FileUtils.listFiles(temporaryFolder, extensions, false);
         assertEquals(1, files.size());
-        Collection<String> filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"));
-        assertFalse(filenames.contains("README"));
-        assertFalse(filenames.contains("dummy-file.txt"));
+        Collection<String> fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"));
+        assertFalse(fileNames.contains("README"));
+        assertFalse(fileNames.contains("dummy-file.txt"));
 
         files = FileUtils.listFiles(temporaryFolder, extensions, true);
-        filenames = filesToFilenames(files);
-        assertEquals(4, filenames.size());
-        assertTrue(filenames.contains("dummy-file.txt"));
-        assertFalse(filenames.contains("dummy-index.html"));
+        fileNames = filesToFilenames(files);
+        assertEquals(4, fileNames.size());
+        assertTrue(fileNames.contains("dummy-file.txt"));
+        assertFalse(fileNames.contains("dummy-index.html"));
 
         files = FileUtils.listFiles(temporaryFolder, null, false);
         assertEquals(2, files.size());
-        filenames = filesToFilenames(files);
-        assertTrue(filenames.contains("dummy-build.xml"));
-        assertTrue(filenames.contains("README"));
-        assertFalse(filenames.contains("dummy-file.txt"));
+        fileNames = filesToFilenames(files);
+        assertTrue(fileNames.contains("dummy-build.xml"));
+        assertTrue(fileNames.contains("README"));
+        assertFalse(fileNames.contains("dummy-file.txt"));
     }
 
 
diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java b/src/test/java/org/apache/commons/io/FileUtilsTest.java
index 4d976c4..2c820c8 100644
--- a/src/test/java/org/apache/commons/io/FileUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java
@@ -40,6 +40,7 @@
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.nio.charset.UnsupportedCharsetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -47,6 +48,7 @@
 import java.nio.file.attribute.AclFileAttributeView;
 import java.nio.file.attribute.FileTime;
 import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -86,6 +88,7 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIf;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
@@ -123,20 +126,6 @@
         }
     }
 
-    // Test helper class to pretend a file is shorter than it is
-    private static class ShorterFile extends File {
-        private static final long serialVersionUID = 1L;
-
-        public ShorterFile(final String pathname) {
-            super(pathname);
-        }
-
-        @Override
-        public long length() {
-            return super.length() - 1;
-        }
-    }
-
     private static final String UTF_8 = StandardCharsets.UTF_8.name();
 
     /** Test data. */
@@ -168,11 +157,6 @@
      */
     private static final ListDirectoryWalker LIST_WALKER = new ListDirectoryWalker();
 
-    /**
-     * Delay in milliseconds to make sure test for "last modified date" are accurate
-     */
-    //private static final int LAST_MODIFIED_DELAY = 600;
-
     private File testFile1;
     private File testFile2;
 
@@ -1452,7 +1436,7 @@
     public void testFileUtils() throws Exception {
         // Loads file from classpath
         final File file1 = new File(tempDirFile, "test.txt");
-        final String filename = file1.getAbsolutePath();
+        final String fileName = file1.getAbsolutePath();
 
         //Create test file on-the-fly (used to be in CVS)
         try (OutputStream out = Files.newOutputStream(file1.toPath())) {
@@ -1461,16 +1445,16 @@
 
         final File file2 = new File(tempDirFile, "test2.txt");
 
-        FileUtils.writeStringToFile(file2, filename, UTF_8);
+        FileUtils.writeStringToFile(file2, fileName, UTF_8);
         assertTrue(file2.exists());
         assertTrue(file2.length() > 0);
 
         final String file2contents = FileUtils.readFileToString(file2, UTF_8);
-        assertEquals(filename, file2contents, "Second file's contents correct");
+        assertEquals(fileName, file2contents, "Second file's contents correct");
 
         assertTrue(file2.delete());
 
-        final String contents = FileUtils.readFileToString(new File(filename), UTF_8);
+        final String contents = FileUtils.readFileToString(new File(fileName), UTF_8);
         assertEquals("This is a test", contents, "FileUtils.fileRead()");
 
     }
@@ -1645,8 +1629,8 @@
     @Test
     public void testIO575() throws IOException {
         final Path sourceDir = Files.createTempDirectory("source-dir");
-        final String filename = "some-file";
-        final Path sourceFile = Files.createFile(sourceDir.resolve(filename));
+        final String fileName = "some-file";
+        final Path sourceFile = Files.createFile(sourceDir.resolve(fileName));
 
         assertEquals(SystemUtils.IS_OS_WINDOWS, sourceFile.toFile().canExecute());
 
@@ -1658,7 +1642,7 @@
 
         FileUtils.copyDirectory(sourceDir.toFile(), destDir.toFile());
 
-        final Path destFile = destDir.resolve(filename);
+        final Path destFile = destDir.resolve(fileName);
 
         assertTrue(destFile.toFile().exists());
         assertTrue(destFile.toFile().canExecute());
@@ -2445,6 +2429,41 @@
     }
 
     @Test
+    public void testReadFileToByteArray_Errors() {
+        assertThrows(NullPointerException.class, () -> FileUtils.readFileToByteArray(null));
+        assertThrows(IOException.class, () -> FileUtils.readFileToByteArray(new File("non-exsistent")));
+        assertThrows(IOException.class, () -> FileUtils.readFileToByteArray(tempDirFile));
+    }
+
+    @Test
+    @EnabledIf("isPosixFilePermissionsSupported")
+    public void testReadFileToByteArray_IOExceptionOnPosixFileSystem() throws Exception {
+        final File file = TestUtils.newFile(tempDirFile, "cant-read.txt");
+        TestUtils.createFile(file, 100);
+        Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("---------"));
+
+        assertThrows(IOException.class, () -> FileUtils.readFileToByteArray(file));
+    }
+
+    @Test
+    public void testReadFileToString_Errors() {
+        assertThrows(NullPointerException.class, () -> FileUtils.readFileToString(null));
+        assertThrows(IOException.class, () -> FileUtils.readFileToString(new File("non-exsistent")));
+        assertThrows(IOException.class, () -> FileUtils.readFileToString(tempDirFile));
+        assertThrows(UnsupportedCharsetException.class, () -> FileUtils.readFileToString(tempDirFile, "unsupported-charset"));
+    }
+
+    @Test
+    @EnabledIf("isPosixFilePermissionsSupported")
+    public void testReadFileToString_IOExceptionOnPosixFileSystem() throws Exception {
+        final File file = TestUtils.newFile(tempDirFile, "cant-read.txt");
+        TestUtils.createFile(file, 100);
+        Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("---------"));
+
+        assertThrows(IOException.class, () -> FileUtils.readFileToString(file));
+    }
+
+    @Test
     public void testReadFileToStringWithDefaultEncoding() throws Exception {
         final File file = new File(tempDirFile, "read.obj");
         final String fixture = "Hello /u1234";
@@ -2478,6 +2497,24 @@
     }
 
     @Test
+    public void testReadLines_Errors() {
+        assertThrows(NullPointerException.class, () -> FileUtils.readLines(null));
+        assertThrows(IOException.class, () -> FileUtils.readLines(new File("non-exsistent")));
+        assertThrows(IOException.class, () -> FileUtils.readLines(tempDirFile));
+        assertThrows(UnsupportedCharsetException.class, () -> FileUtils.readLines(tempDirFile, "unsupported-charset"));
+    }
+
+    @Test
+    @EnabledIf("isPosixFilePermissionsSupported")
+    public void testReadLines_IOExceptionOnPosixFileSystem() throws Exception {
+        final File file = TestUtils.newFile(tempDirFile, "cant-read.txt");
+        TestUtils.createFile(file, 100);
+        Files.setPosixFilePermissions(file.toPath(), PosixFilePermissions.fromString("---------"));
+
+        assertThrows(IOException.class, () -> FileUtils.readLines(file));
+    }
+
+    @Test
     public void testSizeOf() throws Exception {
         final File file = new File(tempDirFile, getName());
 
diff --git a/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java b/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java
index 9db0d67..a492d39 100644
--- a/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java
+++ b/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java
@@ -87,7 +87,7 @@
         try (final InputStream inputStream = getClass().getResourceAsStream(FIXTURE)) {
             bytes = IOUtils.toByteArray(inputStream);
         }
-        final int numSkips = (random.nextInt(bytes.length) / 100) + 1;
+        final int numSkips = random.nextInt(bytes.length) / 100 + 1;
 
         final int[] skips = generateSkips(bytes, numSkips, random);
         final int[] expected;
diff --git a/src/test/java/org/apache/commons/io/IOUtilsTest.java b/src/test/java/org/apache/commons/io/IOUtilsTest.java
index e361e88..c921abc 100644
--- a/src/test/java/org/apache/commons/io/IOUtilsTest.java
+++ b/src/test/java/org/apache/commons/io/IOUtilsTest.java
@@ -1257,7 +1257,7 @@
         assertThrows(NullPointerException.class, () -> IOUtils.resourceToURL(null, ClassLoader.getSystemClassLoader()));
     }
 
-    public void testSingleEOL(final String s1, final String s2, final boolean ifEquals) throws IOException {
+    public void testSingleEOL(final String s1, final String s2, final boolean ifEquals) {
         assertEquals(ifEquals, IOUtils.contentEqualsIgnoreEOL(
                 new CharArrayReader(s1.toCharArray()),
                 new CharArrayReader(s2.toCharArray())
@@ -1750,6 +1750,15 @@
     }
 
     @Test
+    public void testWriteLines() throws IOException {
+        final String[] data = {"The", "quick"};
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        IOUtils.writeLines(Arrays.asList(data), "\n", out, "UTF-16");
+        final String result = new String(out.toByteArray(), StandardCharsets.UTF_16);
+        assertEquals("The\nquick\n", result);
+    }
+
+    @Test
     public void testWriteLittleString() throws IOException {
         final String data = "\uD83D";
         // White-box test to check that not closing the internal channel is not a problem.
diff --git a/src/test/java/org/apache/commons/io/RandomAccessFilesTest.java b/src/test/java/org/apache/commons/io/RandomAccessFilesTest.java
index d387359..ce7fa8f 100644
--- a/src/test/java/org/apache/commons/io/RandomAccessFilesTest.java
+++ b/src/test/java/org/apache/commons/io/RandomAccessFilesTest.java
@@ -19,10 +19,15 @@
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.io.RandomAccessFile;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
 
 import org.junit.jupiter.api.Test;
 
@@ -31,32 +36,101 @@
  */
 public class RandomAccessFilesTest {
 
-    protected static final String FILE_RES_RO = "/org/apache/commons/io/test-file-20byteslength.bin";
-    protected static final String FILE_NAME_RO = "src/test/resources" + FILE_RES_RO;
+    private static final String FILE_NAME_RO_20 = "src/test/resources/org/apache/commons/io/test-file-20byteslength.bin";
+    private static final String FILE_NAME_RO_0 = "src/test/resources/org/apache/commons/io/test-file-empty.bin";
+    private static final String FILE_NAME_RO_0_BIS = "src/test/resources/org/apache/commons/io/test-file-empty2.bin";
+
+    @Test
+    public void testContentEquals() throws IOException {
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
+            assertEquals(raf1, raf1);
+            assertTrue(RandomAccessFiles.contentEquals(raf1, raf1));
+        }
+        // as above, to make sure resources are OK
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
+            assertEquals(raf1, raf1);
+            assertTrue(RandomAccessFiles.contentEquals(raf1, raf1));
+        }
+        // same 20 bytes
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20);
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
+            assertTrue(RandomAccessFiles.contentEquals(raf1, raf2));
+        }
+        // same empty file
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_0);
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_0)) {
+            assertTrue(RandomAccessFiles.contentEquals(raf1, raf2));
+            assertTrue(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf2), RandomAccessFiles.reset(raf1)));
+        }
+        // diff empty file
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_0);
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_0_BIS)) {
+            assertTrue(RandomAccessFiles.contentEquals(raf1, raf2));
+            assertTrue(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf2), RandomAccessFiles.reset(raf1)));
+        }
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_0);
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
+            assertFalse(RandomAccessFiles.contentEquals(raf1, raf2));
+            assertFalse(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf2), RandomAccessFiles.reset(raf1)));
+        }
+        //
+        final Path bigFile1 = Files.createTempFile(getClass().getSimpleName(), "-1.bin");
+        final Path bigFile2 = Files.createTempFile(getClass().getSimpleName(), "-2.bin");
+        final Path bigFile3 = Files.createTempFile(getClass().getSimpleName(), "-3.bin");
+        try {
+            final int newLength = 1_000_000;
+            final byte[] bytes1 = new byte[newLength];
+            final byte[] bytes2 = new byte[newLength];
+            Arrays.fill(bytes1, (byte) 1);
+            Arrays.fill(bytes2, (byte) 2);
+            Files.write(bigFile1, bytes1);
+            Files.write(bigFile2, bytes2);
+            try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(bigFile1);
+                    RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(bigFile2)) {
+                assertFalse(RandomAccessFiles.contentEquals(raf1, raf2));
+                assertFalse(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf2), RandomAccessFiles.reset(raf1)));
+                assertTrue(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf1), RandomAccessFiles.reset(raf1)));
+            }
+            // Make the last byte different
+            final byte[] bytes3 = bytes1.clone();
+            bytes3[bytes3.length - 1] = 9;
+            Files.write(bigFile3, bytes3);
+            try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(bigFile1);
+                    RandomAccessFile raf3 = RandomAccessFileMode.READ_ONLY.create(bigFile3)) {
+                assertFalse(RandomAccessFiles.contentEquals(raf1, raf3));
+                assertFalse(RandomAccessFiles.contentEquals(RandomAccessFiles.reset(raf3), RandomAccessFiles.reset(raf1)));
+            }
+        } finally {
+            // Delete ASAP
+            Files.deleteIfExists(bigFile1);
+            Files.deleteIfExists(bigFile2);
+            Files.deleteIfExists(bigFile3);
+        }
+    }
 
     @Test
     public void testRead() throws IOException {
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             final byte[] buffer = RandomAccessFiles.read(raf, 0, 0);
             assertArrayEquals(new byte[] {}, buffer);
         }
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             final byte[] buffer = RandomAccessFiles.read(raf, 1, 0);
             assertArrayEquals(new byte[] {}, buffer);
         }
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             final byte[] buffer = RandomAccessFiles.read(raf, 0, 1);
             assertArrayEquals(new byte[] { '1' }, buffer);
         }
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             final byte[] buffer = RandomAccessFiles.read(raf, 1, 1);
             assertArrayEquals(new byte[] { '2' }, buffer);
         }
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             final byte[] buffer = RandomAccessFiles.read(raf, 0, 20);
             assertEquals(20, buffer.length);
         }
-        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO)) {
+        try (final RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(FILE_NAME_RO_20)) {
             assertThrows(IOException.class, () -> RandomAccessFiles.read(raf, 0, 21));
         }
     }
diff --git a/src/test/java/org/apache/commons/io/StreamIteratorTest.java b/src/test/java/org/apache/commons/io/StreamIteratorTest.java
new file mode 100644
index 0000000..7cfa9af
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/StreamIteratorTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Iterator;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link StreamIterator}.
+ */
+public class StreamIteratorTest {
+
+    @Test
+    public void testForEachRemaining() {
+        final AtomicBoolean closed = new AtomicBoolean();
+        final Iterator<Integer> iter = StreamIterator.iterator(Stream.of(1, 2, 3).onClose(() -> closed.set(true)));
+        final AtomicInteger sum = new AtomicInteger();
+
+        iter.forEachRemaining(sum::addAndGet);
+
+        assertEquals(6, sum.get());
+        assertTrue(closed.get());
+    }
+
+    @Test
+    public void testHasNext() {
+        final AtomicBoolean closed = new AtomicBoolean();
+        final Iterator<Integer> iter = StreamIterator.iterator(Stream.of(1, 2, 3).onClose(() -> closed.set(true)));
+        int sum = 0;
+
+        while (iter.hasNext()) {
+            sum += iter.next();
+        }
+
+        assertEquals(6, sum);
+        assertTrue(closed.get());
+    }
+}
diff --git a/src/test/java/org/apache/commons/io/file/AbstractTempDirTest.java b/src/test/java/org/apache/commons/io/file/AbstractTempDirTest.java
index a5ed616..16bd66c 100644
--- a/src/test/java/org/apache/commons/io/file/AbstractTempDirTest.java
+++ b/src/test/java/org/apache/commons/io/file/AbstractTempDirTest.java
@@ -19,6 +19,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
@@ -52,4 +53,8 @@
         tempDirFile = tempDirPath.toFile();
     }
 
+
+    protected final boolean isPosixFilePermissionsSupported() {
+        return FileSystems.getDefault().supportedFileAttributeViews().contains("posix");
+    }
 }
diff --git a/src/test/java/org/apache/commons/io/file/FilesUncheckTest.java b/src/test/java/org/apache/commons/io/file/FilesUncheckTest.java
index 4fa7368..80f4d37 100644
--- a/src/test/java/org/apache/commons/io/file/FilesUncheckTest.java
+++ b/src/test/java/org/apache/commons/io/file/FilesUncheckTest.java
@@ -50,6 +50,7 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.io.function.Uncheck;
@@ -166,7 +167,9 @@
 
     @Test
     public void testFind() {
-        assertNotNull(FilesUncheck.find(FILE_PATH_EMPTY, 0, (t, u) -> false));
+        try (Stream<Path> find = FilesUncheck.find(FILE_PATH_EMPTY, 0, (t, u) -> false)) {
+            assertNotNull(find);
+        }
     }
 
     @Test
@@ -207,17 +210,23 @@
 
     @Test
     public void testLinesPath() {
-        assertEquals(0, FilesUncheck.lines(FILE_PATH_EMPTY).count());
+        try (Stream<String> stream = FilesUncheck.lines(FILE_PATH_EMPTY)) {
+            assertEquals(0, stream.count());
+        }
     }
 
     @Test
     public void testLinesPathCharset() {
-        assertEquals(0, FilesUncheck.lines(FILE_PATH_EMPTY, StandardCharsets.UTF_8).count());
+        try (Stream<String> stream = FilesUncheck.lines(FILE_PATH_EMPTY, StandardCharsets.UTF_8)) {
+            assertEquals(0, stream.count());
+        }
     }
 
     @Test
     public void testList() {
-        assertEquals(1, FilesUncheck.list(Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0")).count());
+        try (Stream<Path> stream = FilesUncheck.list(Paths.get("src/test/resources/org/apache/commons/io/dirs-1-file-size-0"))) {
+            assertEquals(1, stream.count());
+        }
     }
 
     @Test
@@ -426,12 +435,16 @@
 
     @Test
     public void testWalkPathFileVisitOptionArray() {
-        assertTrue(0 < FilesUncheck.walk(TARGET_PATH, FileVisitOption.FOLLOW_LINKS).count());
+        try (Stream<Path> stream = FilesUncheck.walk(TARGET_PATH, FileVisitOption.FOLLOW_LINKS)) {
+            assertTrue(0 < stream.count());
+        }
     }
 
     @Test
     public void testWalkPathIntFileVisitOptionArray() {
-        assertEquals(1, FilesUncheck.walk(TARGET_PATH, 0, FileVisitOption.FOLLOW_LINKS).count());
+        try (Stream<Path> stream = FilesUncheck.walk(TARGET_PATH, 0, FileVisitOption.FOLLOW_LINKS)) {
+            assertEquals(1, stream.count());
+        }
     }
 
     @Test
diff --git a/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java b/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java
index 5a99d62..a663e5f 100644
--- a/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java
+++ b/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java
@@ -104,7 +104,7 @@
             final boolean[] trueResults = testTrueResults.get(i);
             final boolean[] falseResults = testFalseResults.get(i);
             final boolean fileResults = testFileResults.get(i);
-            final boolean filenameResults = testFilenameResults.get(i);
+            final boolean fileNameResults = testFilenameResults.get(i);
 
             // Test conditional AND filter created by passing filters to the constructor
             final IOFileFilter filter = this.buildFilterUsingAdd(filters);
@@ -116,10 +116,10 @@
             assertTrueFiltersInvoked(i, trueFilters, trueResults);
             assertFalseFiltersInvoked(i, falseFilters, falseResults);
 
-            // Test as a filename filter
+            // Test as a file name filter
             resetTrueFilters(this.trueFilters);
             resetFalseFilters(this.falseFilters);
-            assertFilenameFiltering(i, filter, this.file, filenameResults);
+            assertFilenameFiltering(i, filter, this.file, fileNameResults);
             assertTrueFiltersInvoked(i, trueFilters, trueResults);
             assertFalseFiltersInvoked(i, falseFilters, falseResults);
         }
@@ -138,7 +138,7 @@
             final boolean[] trueResults = testTrueResults.get(i);
             final boolean[] falseResults = testFalseResults.get(i);
             final boolean fileResults = testFileResults.get(i);
-            final boolean filenameResults = testFilenameResults.get(i);
+            final boolean fileNameResults = testFilenameResults.get(i);
 
             // Test conditional AND filter created by passing filters to the constructor
             final IOFileFilter filter = this.buildFilterUsingConstructor(filters);
@@ -150,10 +150,10 @@
             assertTrueFiltersInvoked(i, trueFilters, trueResults);
             assertFalseFiltersInvoked(i, falseFilters, falseResults);
 
-            // Test as a filename filter
+            // Test as a file name filter
             resetTrueFilters(this.trueFilters);
             resetFalseFilters(this.falseFilters);
-            assertFilenameFiltering(i, filter, this.file, filenameResults);
+            assertFilenameFiltering(i, filter, this.file, fileNameResults);
             assertTrueFiltersInvoked(i, trueFilters, trueResults);
             assertFalseFiltersInvoked(i, falseFilters, falseResults);
         }
diff --git a/src/test/java/org/apache/commons/io/function/IOBaseStreamTest.java b/src/test/java/org/apache/commons/io/function/IOBaseStreamTest.java
index 1c05418..a0bd3e4 100644
--- a/src/test/java/org/apache/commons/io/function/IOBaseStreamTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOBaseStreamTest.java
@@ -67,7 +67,7 @@
     /**
      * Implements IOBaseStream with a concrete type.
      */
-    private static class IOBaseStreamPathFixture<B extends BaseStream<Path, B>> extends IOBaseStreamFixture<Path, IOBaseStreamPathFixture<B>, B> {
+    private static final class IOBaseStreamPathFixture<B extends BaseStream<Path, B>> extends IOBaseStreamFixture<Path, IOBaseStreamPathFixture<B>, B> {
 
         private IOBaseStreamPathFixture(final B baseStream) {
             super(baseStream);
@@ -80,7 +80,7 @@
 
     }
 
-    private static class MyRuntimeException extends RuntimeException {
+    private static final class MyRuntimeException extends RuntimeException {
 
         private static final long serialVersionUID = 1L;
 
diff --git a/src/test/java/org/apache/commons/io/function/IOBinaryOperatorStreamTest.java b/src/test/java/org/apache/commons/io/function/IOBinaryOperatorStreamTest.java
index a591e78..501d75f 100644
--- a/src/test/java/org/apache/commons/io/function/IOBinaryOperatorStreamTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOBinaryOperatorStreamTest.java
@@ -88,14 +88,19 @@
     public void testReduce() throws IOException {
         // A silly example to pass in a IOBinaryOperator.
         final Path current = PathUtils.current();
-        final Path expected = Files.list(current).reduce((t, u) -> {
-            try {
-                return t.toRealPath();
-            } catch (final IOException e) {
-                return fail(e);
-            }
-        }).get();
-        assertEquals(expected, Files.list(current).reduce(REAL_PATH_BO).get());
+        final Path expected;
+        try (Stream<Path> stream = Files.list(current)) {
+            expected = stream.reduce((t, u) -> {
+                try {
+                    return t.toRealPath();
+                } catch (final IOException e) {
+                    return fail(e);
+                }
+            }).get();
+        }
+        try (Stream<Path> stream = Files.list(current)) {
+            assertEquals(expected, stream.reduce(REAL_PATH_BO).get());
+        }
     }
 
 }
diff --git a/src/test/java/org/apache/commons/io/function/IOConsumerTest.java b/src/test/java/org/apache/commons/io/function/IOConsumerTest.java
index 5bfeb40..1871be4 100644
--- a/src/test/java/org/apache/commons/io/function/IOConsumerTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOConsumerTest.java
@@ -76,11 +76,11 @@
     public void testForAllArrayOf1() throws IOException {
         IOConsumer.forAll(TestUtils.throwingIOConsumer(), (String[]) null);
         IOConsumer.forAll(null, (String[]) null);
-        assertThrows(IOExceptionList.class, () -> IOConsumer.forAll(TestUtils.throwingIOConsumer(), new String[] {"1"}));
+        assertThrows(IOExceptionList.class, () -> IOConsumer.forAll(TestUtils.throwingIOConsumer(), "1"));
         //
         final AtomicReference<String> ref = new AtomicReference<>("0");
         final IOConsumer<String> consumer1 = s -> ref.set(ref.get() + s);
-        IOConsumer.forAll(consumer1, new String[] {"1"});
+        IOConsumer.forAll(consumer1, "1");
         assertEquals("01", ref.get());
     }
 
@@ -88,11 +88,11 @@
     public void testForAllArrayOf2() throws IOException {
         IOConsumer.forAll(TestUtils.throwingIOConsumer(), (String[]) null);
         IOConsumer.forAll(null, (String[]) null);
-        assertThrows(IOExceptionList.class, () -> IOConsumer.forAll(TestUtils.throwingIOConsumer(), new String[] {"1", "2"}));
+        assertThrows(IOExceptionList.class, () -> IOConsumer.forAll(TestUtils.throwingIOConsumer(), "1", "2"));
         //
         final AtomicReference<String> ref = new AtomicReference<>("0");
         final IOConsumer<String> consumer1 = s -> ref.set(ref.get() + s);
-        IOConsumer.forAll(consumer1, new String[] {"1", "2"});
+        IOConsumer.forAll(consumer1, "1", "2");
         assertEquals("012", ref.get());
     }
 
diff --git a/src/test/java/org/apache/commons/io/function/IOFunctionTest.java b/src/test/java/org/apache/commons/io/function/IOFunctionTest.java
index 1032aee..af859ab 100644
--- a/src/test/java/org/apache/commons/io/function/IOFunctionTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOFunctionTest.java
@@ -38,7 +38,7 @@
  */
 public class IOFunctionTest {
 
-    private static class Holder<T> {
+    private static final class Holder<T> {
         T value;
     }
 
@@ -168,7 +168,7 @@
     public void testIdentity() throws IOException {
         assertEquals(IOFunction.identity(), IOFunction.identity());
         final IOFunction<byte[], byte[]> identityFunction = IOFunction.identity();
-        final byte[] buf = new byte[] {(byte) 0xa, (byte) 0xb, (byte) 0xc};
+        final byte[] buf = {(byte) 0xa, (byte) 0xb, (byte) 0xc};
         assertEquals(buf, identityFunction.apply(buf));
         assertArrayEquals(buf, identityFunction.apply(buf));
     }
diff --git a/src/test/java/org/apache/commons/io/function/IOLongSupplierTest.java b/src/test/java/org/apache/commons/io/function/IOLongSupplierTest.java
index 07748de..dce0a42 100644
--- a/src/test/java/org/apache/commons/io/function/IOLongSupplierTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOLongSupplierTest.java
@@ -49,7 +49,7 @@
     }
 
     @Test
-    public void testAsSupplier() throws IOException {
+    public void testAsSupplier() {
         assertThrows(UncheckedIOException.class, () -> TestConstants.THROWING_IO_LONG_SUPPLIER.asSupplier().getAsLong());
         assertEquals(1L, getThrowsNone(() -> TestUtils.compareAndSetThrowsIO(atomicLong, 1L)));
         assertEquals(1L, atomicLong.get());
diff --git a/src/test/java/org/apache/commons/io/function/IOStreamTest.java b/src/test/java/org/apache/commons/io/function/IOStreamTest.java
index 198a361..3e2ef9c 100644
--- a/src/test/java/org/apache/commons/io/function/IOStreamTest.java
+++ b/src/test/java/org/apache/commons/io/function/IOStreamTest.java
@@ -402,7 +402,7 @@
 
     @SuppressWarnings("resource") // custom stream not recognized by compiler warning machinery
     @Test
-    public void testOnCloseMultipleHandlers() throws IOException {
+    public void testOnCloseMultipleHandlers() {
         //
         final AtomicReference<String> ref = new AtomicReference<>();
         // Sanity check
@@ -511,13 +511,14 @@
             assertEquals(1, IOStream.of("C", "D").skip(1).peek(e -> compareAndSetRE(ref, null, e)).count());
             assertEquals(1, IOStream.of("C", "D").skip(1).peek(e -> compareAndSetIO(ref, null, e)).count());
             assertNull(ref.get());
-        } else if (AT_LEAST_JAVA_11) {
-            assertThrows(RuntimeException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetRE(ref, null, e)).count());
-            assertThrows(IOException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetIO(ref, null, e)).count());
-            assertEquals("B", ref.get());
         } else {
-            assertThrows(RuntimeException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetRE(ref, null, e)).count());
-            assertThrows(IOException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetIO(ref, null, e)).count());
+            if (AT_LEAST_JAVA_11) {
+                assertThrows(RuntimeException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetRE(ref, null, e)).count());
+                assertThrows(IOException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetIO(ref, null, e)).count());
+            } else {
+                assertThrows(RuntimeException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetRE(ref, null, e)).count());
+                assertThrows(IOException.class, () -> IOStream.of("C", "D").skip(1).peek(e -> compareAndSetIO(ref, null, e)).count());
+            }
             assertEquals("B", ref.get());
         }
     }
diff --git a/src/test/java/org/apache/commons/io/function/TestConstants.java b/src/test/java/org/apache/commons/io/function/TestConstants.java
index bafe7ef..dfea176 100644
--- a/src/test/java/org/apache/commons/io/function/TestConstants.java
+++ b/src/test/java/org/apache/commons/io/function/TestConstants.java
@@ -44,17 +44,17 @@
 
     static IOFunction<Object, Object> THROWING_IO_FUNCTION = t -> throwIOException();
 
-    static IOIntSupplier THROWING_IO_INT_SUPPLIER = () -> throwIOException();
+    static IOIntSupplier THROWING_IO_INT_SUPPLIER = TestConstants::throwIOException;
 
-    static IOLongSupplier THROWING_IO_LONG_SUPPLIER = () -> throwIOException();
+    static IOLongSupplier THROWING_IO_LONG_SUPPLIER = TestConstants::throwIOException;
 
     static IOPredicate<Object> THROWING_IO_PREDICATE = t -> throwIOException();
 
     static IOQuadFunction<Object, Object, Object, Object, Object> THROWING_IO_QUAD_FUNCTION = (t, u, v, w) -> throwIOException();
 
-    static IORunnable THROWING_IO_RUNNABLE = () -> throwIOException();
+    static IORunnable THROWING_IO_RUNNABLE = TestConstants::throwIOException;
 
-    static IOSupplier<Object> THROWING_IO_SUPPLIER = () -> throwIOException();
+    static IOSupplier<Object> THROWING_IO_SUPPLIER = TestConstants::throwIOException;
 
     static IOTriConsumer<Object, Object, Object> THROWING_IO_TRI_CONSUMER = (t, u, v) -> throwIOException();
 
diff --git a/src/test/java/org/apache/commons/io/input/BOMInputStreamTest.java b/src/test/java/org/apache/commons/io/input/BOMInputStreamTest.java
index a79cb1c..9ce5d54 100644
--- a/src/test/java/org/apache/commons/io/input/BOMInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/BOMInputStreamTest.java
@@ -52,7 +52,7 @@
     /**
      *  A mock InputStream that expects {@code close()} to be called.
      */
-    private static class ExpectCloseInputStream extends InputStream {
+    private static final class ExpectCloseInputStream extends InputStream {
         private boolean _closeCalled;
 
         public void assertCloseCalled() {
@@ -415,7 +415,7 @@
         assertThrows(IllegalArgumentException.class, () -> BOMInputStream.builder()
                 .setInputStream(createUtf8Input(data, true))
                 .setInclude(true)
-                .setByteOrderMarks(new ByteOrderMark[0])
+                .setByteOrderMarks()
                 .get()
                 .close());
     }
diff --git a/src/test/java/org/apache/commons/io/input/CharSequenceInputStreamTest.java b/src/test/java/org/apache/commons/io/input/CharSequenceInputStreamTest.java
index 7bc997e..a8e06b8 100644
--- a/src/test/java/org/apache/commons/io/input/CharSequenceInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/CharSequenceInputStreamTest.java
@@ -32,9 +32,12 @@
 import java.util.Set;
 
 import org.apache.commons.io.Charsets;
+import org.apache.commons.io.CharsetsTest;
 import org.apache.commons.io.IOUtils;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class CharSequenceInputStreamTest {
 
@@ -78,11 +81,11 @@
                 "Shift_JIS".equalsIgnoreCase(csName);
     }
 
-    @Test
-    public void testAvailable() throws Exception {
-        for (final String csName : Charset.availableCharsets().keySet()) {
-            // prevent java.lang.UnsupportedOperationException at sun.nio.cs.ext.ISO2022_CN.newEncoder.
-            // also try and avoid the following exception
+    @ParameterizedTest(name = "{0}")
+    @MethodSource(CharsetsTest.AVAIL_CHARSETS)
+    public void testAvailable(final String csName) throws Exception {
+        // prevent java.lang.UnsupportedOperationException at sun.nio.cs.ext.ISO2022_CN.newEncoder.
+        // also try and avoid the following exception
 //            java.lang.UnsupportedOperationException: null
 //            at java.nio.CharBuffer.array(CharBuffer.java:940)
 //            at sun.nio.cs.ext.COMPOUND_TEXT_Encoder.encodeLoop(COMPOUND_TEXT_Encoder.java:75)
@@ -92,14 +95,13 @@
 //            at org.apache.commons.io.input.CharSequenceInputStreamTest.testAvailableRead(CharSequenceInputStreamTest.java:412)
 //            at org.apache.commons.io.input.CharSequenceInputStreamTest.testAvailable(CharSequenceInputStreamTest.java:424)
 
-            try {
-                if (isAvailabilityTestableForCharset(csName)) {
-                    testAvailableSkip(csName);
-                    testAvailableRead(csName);
-                }
-            } catch (final UnsupportedOperationException e){
-                fail("Operation not supported for " + csName);
+        try {
+            if (isAvailabilityTestableForCharset(csName)) {
+                testAvailableSkip(csName);
+                testAvailableRead(csName);
             }
+        } catch (final UnsupportedOperationException e) {
+            fail("Operation not supported for " + csName);
         }
     }
 
@@ -157,13 +159,12 @@
 //        at sun.nio.cs.ext.COMPOUND_TEXT_Encoder.encodeLoop(COMPOUND_TEXT_Encoder.java:75)
 //        at java.nio.charset.CharsetEncoder.encode(CharsetEncoder.java:544)
 //        at org.apache.commons.io.input.CharSequenceInputStream.fillBuffer(CharSequenceInputStream.java:111)
-    @Test
-    public void testBufferedRead_AvailableCharset() throws IOException {
-        for (final String csName : Charset.availableCharsets().keySet()) {
-            // prevent java.lang.UnsupportedOperationException at sun.nio.cs.ext.ISO2022_CN.newEncoder.
-            if (isAvailabilityTestableForCharset(csName)) {
-                testBufferedRead(TEST_STRING, csName);
-            }
+    @ParameterizedTest(name = "{0}")
+    @MethodSource(CharsetsTest.AVAIL_CHARSETS)
+    public void testBufferedRead_AvailableCharset(final String csName) throws IOException {
+        // prevent java.lang.UnsupportedOperationException at sun.nio.cs.ext.ISO2022_CN.newEncoder.
+        if (isAvailabilityTestableForCharset(csName)) {
+            testBufferedRead(TEST_STRING, csName);
         }
     }
 
diff --git a/src/test/java/org/apache/commons/io/input/ClassLoaderObjectInputStreamTest.java b/src/test/java/org/apache/commons/io/input/ClassLoaderObjectInputStreamTest.java
index 2e8d0f5..3b341ba 100644
--- a/src/test/java/org/apache/commons/io/input/ClassLoaderObjectInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/ClassLoaderObjectInputStreamTest.java
@@ -43,7 +43,7 @@
         A, B, C
     }
 
-    private static class Test implements Serializable {
+    private static final class Test implements Serializable {
         private static final long serialVersionUID = 1L;
         private final int i;
 
diff --git a/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java b/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java
index a72e0f3..ba0944e 100644
--- a/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java
@@ -28,7 +28,7 @@
 
 public class MarkShieldInputStreamTest {
 
-    private static class MarkTestableInputStream extends ProxyInputStream {
+    private static final class MarkTestableInputStream extends ProxyInputStream {
         int markcount;
         int readlimit;
 
diff --git a/src/test/java/org/apache/commons/io/input/MessageDigestCalculatingInputStreamTest.java b/src/test/java/org/apache/commons/io/input/MessageDigestCalculatingInputStreamTest.java
index 517a014..9123c41 100644
--- a/src/test/java/org/apache/commons/io/input/MessageDigestCalculatingInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/MessageDigestCalculatingInputStreamTest.java
@@ -20,40 +20,57 @@
 
 import java.io.ByteArrayInputStream;
 import java.security.MessageDigest;
-import java.util.Random;
 
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.codec.digest.MessageDigestAlgorithms;
+import org.apache.commons.io.input.MessageDigestCalculatingInputStream.Builder;
 import org.junit.jupiter.api.Test;
 
 /**
  * Tests {@link MessageDigestCalculatingInputStream}.
  */
+@SuppressWarnings("deprecation")
 public class MessageDigestCalculatingInputStreamTest {
 
-    public static byte[] generateRandomByteStream(final int pSize) {
-        final byte[] buffer = new byte[pSize];
-        final Random rnd = new Random();
-        rnd.nextBytes(buffer);
-        return buffer;
-    }
-
     @Test
-    public void test() throws Exception {
+    public void testNormalUse() throws Exception {
         for (int i = 256; i < 8192; i = i * 2) {
-            final byte[] buffer = generateRandomByteStream(i);
-            final MessageDigest messageDigest = MessageDigestCalculatingInputStream.getDefaultMessageDigest();
-            final byte[] expect = messageDigest.digest(buffer);
+            final byte[] buffer = MessageDigestInputStreamTest.generateRandomByteStream(i);
+            final MessageDigest defaultMessageDigest = MessageDigestCalculatingInputStream.getDefaultMessageDigest();
+            final byte[] defaultExpect = defaultMessageDigest.digest(buffer);
+            // Defaults
             try (MessageDigestCalculatingInputStream messageDigestInputStream = new MessageDigestCalculatingInputStream(new ByteArrayInputStream(buffer))) {
                 messageDigestInputStream.consume();
-                assertArrayEquals(expect, messageDigestInputStream.getMessageDigest().digest());
+                assertArrayEquals(defaultExpect, messageDigestInputStream.getMessageDigest().digest());
             }
             try (MessageDigestCalculatingInputStream messageDigestInputStream = MessageDigestCalculatingInputStream.builder()
                     .setInputStream(new ByteArrayInputStream(buffer)).get()) {
                 messageDigestInputStream.consume();
-                assertArrayEquals(expect, messageDigestInputStream.getMessageDigest().digest());
+                assertArrayEquals(defaultExpect, messageDigestInputStream.getMessageDigest().digest());
             }
             try (MessageDigestCalculatingInputStream messageDigestInputStream = MessageDigestCalculatingInputStream.builder().setByteArray(buffer).get()) {
                 messageDigestInputStream.consume();
-                assertArrayEquals(expect, messageDigestInputStream.getMessageDigest().digest());
+                assertArrayEquals(defaultExpect, messageDigestInputStream.getMessageDigest().digest());
+            }
+            // SHA-512
+            final byte[] sha512Expect = DigestUtils.sha512(buffer);
+            {
+                final Builder builder = MessageDigestCalculatingInputStream.builder();
+                builder.setMessageDigest(MessageDigestAlgorithms.SHA_512);
+                builder.setInputStream(new ByteArrayInputStream(buffer));
+                try (MessageDigestCalculatingInputStream messageDigestInputStream = builder.get()) {
+                    messageDigestInputStream.consume();
+                    assertArrayEquals(sha512Expect, messageDigestInputStream.getMessageDigest().digest());
+                }
+            }
+            {
+                final Builder builder = MessageDigestCalculatingInputStream.builder();
+                builder.setMessageDigest(MessageDigestAlgorithms.SHA_512);
+                builder.setInputStream(new ByteArrayInputStream(buffer));
+                try (MessageDigestCalculatingInputStream messageDigestInputStream = builder.get()) {
+                    messageDigestInputStream.consume();
+                    assertArrayEquals(sha512Expect, messageDigestInputStream.getMessageDigest().digest());
+                }
             }
         }
     }
diff --git a/src/test/java/org/apache/commons/io/input/MessageDigestInputStreamTest.java b/src/test/java/org/apache/commons/io/input/MessageDigestInputStreamTest.java
new file mode 100644
index 0000000..d41da0b
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/input/MessageDigestInputStreamTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.util.Random;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.codec.digest.MessageDigestAlgorithms;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link MessageDigestInputStream}.
+ */
+public class MessageDigestInputStreamTest {
+
+    static byte[] generateRandomByteStream(final int pSize) {
+        final byte[] buffer = new byte[pSize];
+        final Random rnd = new Random();
+        rnd.nextBytes(buffer);
+        return buffer;
+    }
+
+    @Test
+    public void testNoDefault() throws Exception {
+        assertThrows(IllegalStateException.class, () -> MessageDigestInputStream.builder().get());
+        assertThrows(NullPointerException.class, () -> MessageDigestInputStream.builder().setInputStream(new ByteArrayInputStream(new byte[] { 1 })).get());
+    }
+
+    @Test
+    public void testNormalUse() throws Exception {
+        for (int i = 256; i < 8192; i = i * 2) {
+            final byte[] buffer = generateRandomByteStream(i);
+            final byte[] expect = DigestUtils.sha512(buffer);
+            try (MessageDigestInputStream messageDigestInputStream = MessageDigestInputStream.builder().setMessageDigest(MessageDigestAlgorithms.SHA_512)
+                    .setInputStream(new ByteArrayInputStream(buffer)).get()) {
+                messageDigestInputStream.consume();
+                assertArrayEquals(expect, messageDigestInputStream.getMessageDigest().digest());
+            }
+            try (MessageDigestInputStream messageDigestInputStream = MessageDigestInputStream.builder().setByteArray(buffer)
+                    .setMessageDigest(DigestUtils.getSha512Digest()).get()) {
+                messageDigestInputStream.consume();
+                assertArrayEquals(expect, messageDigestInputStream.getMessageDigest().digest());
+            }
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/io/input/ObservableInputStreamTest.java b/src/test/java/org/apache/commons/io/input/ObservableInputStreamTest.java
index 0a0e915..8c099ad 100644
--- a/src/test/java/org/apache/commons/io/input/ObservableInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/ObservableInputStreamTest.java
@@ -35,7 +35,7 @@
  */
 public class ObservableInputStreamTest {
 
-    private static class DataViewObserver extends MethodCountObserver {
+    private static final class DataViewObserver extends MethodCountObserver {
         private byte[] buffer;
         private int lastValue = -1;
         private int length = -1;
@@ -55,7 +55,7 @@
         }
     }
 
-    private static class LengthObserver extends Observer {
+    private static final class LengthObserver extends Observer {
         private long total;
 
         @Override
@@ -153,8 +153,7 @@
      */
     @Test
     public void testDataByteCalled_add() throws Exception {
-        final byte[] buffer = MessageDigestCalculatingInputStreamTest
-            .generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
+        final byte[] buffer = MessageDigestInputStreamTest.generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
         final DataViewObserver lko = new DataViewObserver();
         try (ObservableInputStream ois = new ObservableInputStream(new ByteArrayInputStream(buffer))) {
             assertEquals(-1, lko.lastValue);
@@ -185,8 +184,7 @@
      */
     @Test
     public void testDataByteCalled_ctor() throws Exception {
-        final byte[] buffer = MessageDigestCalculatingInputStreamTest
-            .generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
+        final byte[] buffer = MessageDigestInputStreamTest.generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
         final DataViewObserver lko = new DataViewObserver();
         try (ObservableInputStream ois = new ObservableInputStream(new ByteArrayInputStream(buffer), lko)) {
             assertEquals(-1, lko.lastValue);
@@ -216,10 +214,9 @@
      */
     @Test
     public void testDataBytesCalled() throws Exception {
-        final byte[] buffer = MessageDigestCalculatingInputStreamTest
-            .generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
+        final byte[] buffer = MessageDigestInputStreamTest.generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
         try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
-            final ObservableInputStream ois = new ObservableInputStream(bais)) {
+                final ObservableInputStream ois = new ObservableInputStream(bais)) {
             final DataViewObserver observer = new DataViewObserver();
             final byte[] readBuffer = new byte[23];
             assertNull(observer.buffer);
diff --git a/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java b/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java
index b176d48..110d11d 100644
--- a/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java
+++ b/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java
@@ -28,7 +28,7 @@
 public class ProxyReaderTest {
 
     /** Custom NullReader implementation. */
-    private static class CustomNullReader extends NullReader {
+    private static final class CustomNullReader extends NullReader {
         CustomNullReader(final int len) {
             super(len);
         }
@@ -45,7 +45,7 @@
     }
 
     /** ProxyReader implementation. */
-    private static class ProxyReaderImpl extends ProxyReader {
+    private static final class ProxyReaderImpl extends ProxyReader {
         ProxyReaderImpl(final Reader proxy) {
             super(proxy);
         }
diff --git a/src/test/java/org/apache/commons/io/input/TailerTest.java b/src/test/java/org/apache/commons/io/input/TailerTest.java
index f0e1f34..783e171 100644
--- a/src/test/java/org/apache/commons/io/input/TailerTest.java
+++ b/src/test/java/org/apache/commons/io/input/TailerTest.java
@@ -60,7 +60,7 @@
  */
 public class TailerTest {
 
-    private static class NonStandardTailable implements Tailer.Tailable {
+    private static final class NonStandardTailable implements Tailer.Tailable {
 
         private final File file;
 
@@ -115,7 +115,7 @@
     /**
      * Test {@link TailerListener} implementation.
      */
-    private static class TestTailerListener extends TailerListenerAdapter {
+    private static final class TestTailerListener extends TailerListenerAdapter {
 
         // Must be synchronized because it is written by one thread and read by another
         private final List<String> lines = Collections.synchronizedList(new ArrayList<>());
@@ -266,16 +266,6 @@
     }
 
     @Test
-    public void testCreatorWithDelayAndFromStartWithReopen() throws Exception {
-        final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen.txt");
-        createFile(file, 0);
-        final TestTailerListener listener = new TestTailerListener(1);
-        try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, false)) {
-            validateTailer(listener, file);
-        }
-    }
-
-    @Test
     public void testCreateWithDelay() throws Exception {
         final File file = new File(temporaryFolder, "tailer-create-with-delay.txt");
         createFile(file, 0);
@@ -325,6 +315,16 @@
         }
     }
 
+    @Test
+    public void testCreatorWithDelayAndFromStartWithReopen() throws Exception {
+        final File file = new File(temporaryFolder, "tailer-create-with-delay-and-from-start-with-reopen.txt");
+        createFile(file, 0);
+        final TestTailerListener listener = new TestTailerListener(1);
+        try (Tailer tailer = Tailer.create(file, listener, TEST_DELAY_MILLIS, false, false)) {
+            validateTailer(listener, file);
+        }
+    }
+
     /*
      * Tests [IO-357][Tailer] InterruptedException while the thread is sleeping is silently ignored.
      */
diff --git a/src/test/java/org/apache/commons/io/input/TimestampedObserverTest.java b/src/test/java/org/apache/commons/io/input/TimestampedObserverTest.java
index 5492200..ea37b38 100644
--- a/src/test/java/org/apache/commons/io/input/TimestampedObserverTest.java
+++ b/src/test/java/org/apache/commons/io/input/TimestampedObserverTest.java
@@ -51,7 +51,7 @@
         assertTrue(timestampedObserver.getOpenToNowDuration().toNanos() > 0);
         assertNull(timestampedObserver.getCloseInstant());
         assertFalse(timestampedObserver.isClosed());
-        final byte[] buffer = MessageDigestCalculatingInputStreamTest.generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
+        final byte[] buffer = MessageDigestInputStreamTest.generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
         try (ObservableInputStream ois = new ObservableInputStream(new ByteArrayInputStream(buffer), timestampedObserver)) {
             assertTrue(timestampedObserver.getOpenInstant().isAfter(before));
             assertTrue(timestampedObserver.getOpenToNowDuration().toNanos() > 0);
@@ -68,7 +68,7 @@
     @Test
     public void testExample() throws IOException {
         final TimestampedObserver timestampedObserver = new TimestampedObserver();
-        final byte[] buffer = MessageDigestCalculatingInputStreamTest
+        final byte[] buffer = MessageDigestInputStreamTest
             .generateRandomByteStream(IOUtils.DEFAULT_BUFFER_SIZE);
         try (ObservableInputStream ois = new ObservableInputStream(new ByteArrayInputStream(buffer),
             timestampedObserver)) {
diff --git a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java
index f7fcba8..d2e969c 100644
--- a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStreamTest.java
@@ -21,7 +21,6 @@
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -31,6 +30,7 @@
 import java.nio.file.Path;
 
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.UnsynchronizedBufferedInputStream.Builder;
 import org.apache.commons.lang3.StringUtils;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
@@ -50,12 +50,16 @@
 
     Path fileName;
 
-    private BufferedInputStream is;
+    private UnsynchronizedBufferedInputStream is;
 
     private InputStream isFile;
 
     byte[] ibuf = new byte[BUFFER_SIZE];
 
+    private Builder builder() {
+        return new UnsynchronizedBufferedInputStream.Builder();
+    }
+
     /**
      * Sets up the fixture, for example, open a network connection. This method is called before a test is executed.
      *
@@ -67,7 +71,7 @@
         Files.write(fileName, DATA.getBytes(StandardCharsets.UTF_8));
 
         isFile = Files.newInputStream(fileName);
-        is = new BufferedInputStream(isFile);
+        is = builder().setInputStream(isFile).get();
     }
 
     /**
@@ -82,7 +86,7 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#available()
+     * Tests {@link UnsynchronizedBufferedInputStream#available()}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -91,7 +95,8 @@
         assertEquals(DATA.length(), is.available(), "Returned incorrect number of available bytes");
 
         // Test that a closed stream throws an IOE for available()
-        final BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' }));
+        final UnsynchronizedBufferedInputStream bis = builder()
+                .setInputStream(new ByteArrayInputStream(new byte[] { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' })).get();
         final int available = bis.available();
         bis.close();
         assertTrue(available != 0);
@@ -100,18 +105,13 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#close()
+     * Tests {@link UnsynchronizedBufferedInputStream#close()}.
      *
      * @throws IOException Thrown on test failure.
      */
     @Test
     public void test_close() throws IOException {
-        new BufferedInputStream(isFile).close();
-
-        // regression for HARMONY-667
-        try (BufferedInputStream buf = new BufferedInputStream(null, 5)) {
-            // closes
-        }
+        builder().setInputStream(isFile).get().close();
 
         try (InputStream in = new InputStream() {
             Object lock = new Object();
@@ -140,7 +140,7 @@
                 return 1;
             }
         }) {
-            final BufferedInputStream bufin = new BufferedInputStream(in);
+            final UnsynchronizedBufferedInputStream bufin = builder().setInputStream(in).get();
             final Thread thread = new Thread(() -> {
                 try {
                     Thread.sleep(1000);
@@ -155,28 +155,24 @@
     }
 
     /*
-     * Tests java.io.BufferedInputStream(InputStream)
+     * Tests {@link UnsynchronizedBufferedInputStream#Builder()}.
      */
     @Test
-    public void test_ConstructorLjava_io_InputStream() throws IOException {
-        try (BufferedInputStream str = new BufferedInputStream(null)) {
-            assertThrows(IOException.class, () -> str.read(), "Expected an IOException");
-        }
+    public void test_ConstructorLjava_io_InputStream() {
+        assertThrows(NullPointerException.class, () -> builder().setInputStream(null).get());
     }
 
     /*
-     * Tests java.io.BufferedInputStream(InputStream)
+     * Tests {@link UnsynchronizedBufferedInputStream#Builder()}.
      */
     @Test
     public void test_ConstructorLjava_io_InputStreamI() throws IOException {
-        try (BufferedInputStream str = new BufferedInputStream(null, 1)) {
-            assertThrows(IOException.class, () -> str.read(), "Expected an IOException");
-        }
+        assertThrows(NullPointerException.class, () -> builder().setInputStream(null).setBufferSize(1).get());
 
-        // Test for method java.io.BufferedInputStream(java.io.InputStream, int)
+        // Test for method UnsynchronizedBufferedInputStream(InputStream, int)
 
         // Create buffer with exact size of file
-        is = new BufferedInputStream(isFile, this.DATA.length());
+        is = builder().setInputStream(isFile).setBufferSize(DATA.length()).get();
         // Ensure buffer gets filled by evaluating one read
         is.read();
         // Close underlying FileInputStream, all but 1 buffered bytes should
@@ -184,16 +180,17 @@
         isFile.close();
         // Read the remaining buffered characters, no IOException should
         // occur.
-        is.skip(this.DATA.length() - 2);
+        is.skip(DATA.length() - 2);
         is.read();
         // is.read should now throw an exception because it will have to be filled.
         assertThrows(IOException.class, () -> is.read());
 
-        assertThrows(NullPointerException.class, () -> UnsynchronizedBufferedInputStream.builder().setInputStream(null).setBufferSize(100).get());
+        assertThrows(NullPointerException.class, () -> builder().setInputStream(null).setBufferSize(100).get());
+        assertThrows(NullPointerException.class, () -> builder().setInputStream(null));
     }
 
     /**
-     * Tests java.io.BufferedInputStream#mark(int)
+     * Tests {@link UnsynchronizedBufferedInputStream#mark(int)}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -213,21 +210,21 @@
         for (int i = 0; i < 256; i++) {
             bytes[i] = (byte) i;
         }
-        InputStream in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12);
+        InputStream in = builder().setInputStream(new ByteArrayInputStream(bytes)).setBufferSize(12).get();
         in.skip(6);
         in.mark(14);
         in.read(new byte[14], 0, 14);
         in.reset();
         assertTrue(in.read() == 6 && in.read() == 7, "Wrong bytes");
 
-        in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12);
+        in = builder().setInputStream(new ByteArrayInputStream(bytes)).setBufferSize(12).get();
         in.skip(6);
         in.mark(8);
         in.skip(7);
         in.reset();
         assertTrue(in.read() == 6 && in.read() == 7, "Wrong bytes 2");
 
-        BufferedInputStream buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2);
+        UnsynchronizedBufferedInputStream buf = builder().setInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 })).setBufferSize(2).get();
         buf.mark(3);
         bytes = new byte[3];
         int result = buf.read(bytes);
@@ -237,7 +234,7 @@
         assertEquals(2, bytes[2], "Assert 2:");
         assertEquals(3, buf.read(), "Assert 3:");
 
-        buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2);
+        buf = builder().setInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 })).setBufferSize(2).get();
         buf.mark(3);
         bytes = new byte[4];
         result = buf.read(bytes);
@@ -249,14 +246,14 @@
         assertEquals(4, buf.read(), "Assert 8:");
         assertEquals(-1, buf.read(), "Assert 9:");
 
-        buf = new BufferedInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 }), 2);
+        buf = builder().setInputStream(new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4 })).setBufferSize(2).get();
         buf.mark(Integer.MAX_VALUE);
         buf.read();
         buf.close();
     }
 
     /**
-     * Tests java.io.BufferedInputStream#markSupported()
+     * Tests {@link UnsynchronizedBufferedInputStream#markSupported()}.
      */
     @Test
     public void test_markSupported() {
@@ -264,7 +261,7 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#read()
+     * Tests {@link UnsynchronizedBufferedInputStream#read()}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -278,7 +275,7 @@
         for (int i = 0; i < 256; i++) {
             bytes[i] = (byte) i;
         }
-        final InputStream in = new BufferedInputStream(new ByteArrayInputStream(bytes), 12);
+        final InputStream in = builder().setInputStream(new ByteArrayInputStream(bytes)).setBufferSize(12).get();
         assertEquals(0, in.read(), "Wrong initial byte"); // Fill the buffer
         final byte[] buf = new byte[14];
         in.read(buf, 0, 14); // Read greater than the buffer
@@ -287,7 +284,7 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#read(byte[], int, int)
+     * Tests {@link UnsynchronizedBufferedInputStream#read(byte[], int, int)}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -299,7 +296,7 @@
         is.read(buf1, 0, buf1.length);
         assertTrue(new String(buf1, 0, buf1.length).equals(DATA.substring(3000, 3100)), "Failed to read correct data");
 
-        try (BufferedInputStream bufin = new BufferedInputStream(new InputStream() {
+        try (UnsynchronizedBufferedInputStream bufin = builder().setInputStream(new InputStream() {
             int size = 2, pos = 0;
 
             byte[] contents = new byte[size];
@@ -330,7 +327,7 @@
                 pos += toRead;
                 return toRead;
             }
-        })) {
+        }).get()) {
             bufin.read();
             final int result = bufin.read(new byte[2], 0, 2);
             assertEquals(1, result, () -> "Incorrect result: " + result);
@@ -338,26 +335,7 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#read(byte[], int, int)
-     *
-     * @throws IOException Thrown on test failure.
-     */
-    @Test
-    public void test_read$BII_Exception() throws IOException {
-        final BufferedInputStream bis = new BufferedInputStream(null);
-        assertThrows(NullPointerException.class, () -> bis.read(null, -1, -1));
-
-        assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], -1, -1));
-        assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], 1, -1));
-        assertThrows(IndexOutOfBoundsException.class, () -> bis.read(new byte[0], 1, 1));
-
-        bis.close();
-
-        assertThrows(IOException.class, () -> bis.read(null, -1, -1));
-    }
-
-    /**
-     * Tests java.io.BufferedInputStream#reset()
+     * Tests {@link UnsynchronizedBufferedInputStream#reset()}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -372,7 +350,7 @@
         is.reset();
         assertTrue(new String(buf1, 0, buf1.length).equals(new String(buf2, 0, buf2.length)), "Reset failed");
 
-        final BufferedInputStream bIn = new BufferedInputStream(new ByteArrayInputStream("1234567890".getBytes()));
+        final UnsynchronizedBufferedInputStream bIn = builder().setInputStream(new ByteArrayInputStream("1234567890".getBytes())).get();
         bIn.mark(10);
         for (int i = 0; i < 11; i++) {
             bIn.read();
@@ -381,36 +359,14 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#reset()
-     *
-     * @throws IOException Thrown on test failure.
-     */
-    @Test
-    public void test_reset_Exception() throws IOException {
-        final BufferedInputStream bis = new BufferedInputStream(null);
-
-        // throws IOException with message "Mark has been invalidated"
-        assertThrows(IOException.class, () -> bis.reset());
-
-        // does not throw IOException
-        bis.mark(1);
-        bis.reset();
-
-        bis.close();
-
-        // throws IOException with message "stream is closed"
-        assertThrows(IOException.class, () -> bis.reset());
-    }
-
-    /**
-     * Tests java.io.BufferedInputStream#reset()
+     * Tests {@link UnsynchronizedBufferedInputStream#reset()}.
      *
      * @throws IOException Thrown on test failure.
      */
     @Test
     public void test_reset_scenario1() throws IOException {
         final byte[] input = "12345678900".getBytes();
-        final BufferedInputStream bufin = new BufferedInputStream(new ByteArrayInputStream(input));
+        final UnsynchronizedBufferedInputStream bufin = builder().setInputStream(new ByteArrayInputStream(input)).get();
         bufin.read();
         bufin.mark(5);
         bufin.skip(5);
@@ -418,33 +374,31 @@
     }
 
     /**
-     * Tests java.io.BufferedInputStream#reset()
+     * Tests {@link UnsynchronizedBufferedInputStream#reset()}.
      *
      * @throws IOException Thrown on test failure.
      */
     @Test
     public void test_reset_scenario2() throws IOException {
         final byte[] input = "12345678900".getBytes();
-        final BufferedInputStream bufin = new BufferedInputStream(new ByteArrayInputStream(input));
+        final UnsynchronizedBufferedInputStream bufin = builder().setInputStream(new ByteArrayInputStream(input)).get();
         bufin.mark(5);
         bufin.skip(6);
         bufin.reset();
     }
 
     /**
-     * Tests java.io.BufferedInputStream#skip(long)
+     * Tests {@link UnsynchronizedBufferedInputStream#skip(long)}.
      *
      * @throws IOException Thrown on test failure.
      */
     @Test
     public void test_skip_NullInputStream() throws IOException {
-        try (BufferedInputStream buf = new BufferedInputStream(null, 5)) {
-            assertEquals(0, buf.skip(0));
-        }
+        assertThrows(NullPointerException.class, () -> builder().setInputStream(null).setBufferSize(5).get());
     }
 
     /**
-     * Tests java.io.BufferedInputStream#skip(long)
+     * Tests {@link UnsynchronizedBufferedInputStream#skip(long)}.
      *
      * @throws IOException Thrown on test failure.
      */
@@ -456,10 +410,5 @@
         is.read(buf1, 0, buf1.length);
         is.reset();
         assertTrue(new String(buf1, 0, buf1.length).equals(DATA.substring(1000, 1010)), "Failed to skip to correct position");
-
-        // regression for HARMONY-667
-        try (BufferedInputStream buf = new BufferedInputStream(null, 5)) {
-            assertThrows(IOException.class, () -> buf.skip(10));
-        }
     }
 }
diff --git a/src/test/java/org/apache/commons/io/input/XmlStreamReaderTest.java b/src/test/java/org/apache/commons/io/input/XmlStreamReaderTest.java
index 555cf7c..63d587a 100644
--- a/src/test/java/org/apache/commons/io/input/XmlStreamReaderTest.java
+++ b/src/test/java/org/apache/commons/io/input/XmlStreamReaderTest.java
@@ -30,6 +30,7 @@
 import java.io.Writer;
 import java.net.URL;
 import java.net.URLConnection;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -37,10 +38,15 @@
 import java.nio.file.StandardOpenOption;
 import java.text.MessageFormat;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
+import org.apache.commons.io.CharsetsTest;
 import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.function.IOFunction;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
 import org.junitpioneer.jupiter.DefaultLocale;
 
 public class XmlStreamReaderTest {
@@ -162,6 +168,14 @@
         return new ByteArrayInputStream(baos.toByteArray());
     }
 
+    private void parseCharset(final String hdr, final String enc, final IOFunction<InputStream, XmlStreamReader> factory) throws Exception {
+        try (final InputStream stream = new ByteArrayInputStream(hdr.getBytes(StandardCharsets.UTF_8))) {
+            try (final XmlStreamReader xml = factory.apply(stream)) {
+                assertEquals(enc.toUpperCase(Locale.ROOT), xml.getEncoding(), enc);
+            }
+        }
+    }
+
     public void testAlternateDefaultEncoding(final String contentType, final String bomEnc, final String streamEnc, final String prologEnc,
             final String alternateEnc) throws Exception {
         try (InputStream is = getXmlInputStream(bomEnc, prologEnc == null ? XML1 : XML3, streamEnc, prologEnc);
@@ -276,6 +290,8 @@
         assertThrows(NullPointerException.class, () -> new XmlStreamReader((URL) null));
     }
 
+    // XML Stream generator
+
     @Test
     public void testEncodingAttributeXML() throws Exception {
         try (InputStream is = new ByteArrayInputStream(ENCODING_ATTRIBUTE_XML.getBytes(StandardCharsets.UTF_8));
@@ -294,8 +310,6 @@
         }
     }
 
-    // XML Stream generator
-
     @Test
     public void testHttp() throws Exception {
         // niallp 2010-10-06 - remove following 2 tests - I reinstated
@@ -431,6 +445,20 @@
         }
     }
 
+    @ParameterizedTest(name = "{0}")
+    @MethodSource(CharsetsTest.AVAIL_CHARSETS)
+    public void testIO_815(final String csName) throws Exception {
+        final MessageFormat fmt = new MessageFormat("<?xml version=\"1.0\" encoding=''{0}''?>\n<root>text</root>");
+        final IOFunction<InputStream, XmlStreamReader> factoryCtor = XmlStreamReader::new;
+        final IOFunction<InputStream, XmlStreamReader> factoryBuilder = stream -> XmlStreamReader.builder().setInputStream(stream).get();
+        parseCharset(fmt.format(new Object[] { csName }), csName, factoryCtor);
+        parseCharset(fmt.format(new Object[] { csName }), csName, factoryBuilder);
+        for (final String alias : Charset.forName(csName).aliases()) {
+            parseCharset(fmt.format(new Object[] { alias }), alias, factoryCtor);
+            parseCharset(fmt.format(new Object[] { alias }), alias, factoryBuilder);
+        }
+    }
+
     // Turkish language has specific rules to convert dotted and dotless i character.
     @Test
     @DefaultLocale(language = "tr")
diff --git a/src/test/java/org/apache/commons/io/input/XmlStreamReaderUtilitiesTest.java b/src/test/java/org/apache/commons/io/input/XmlStreamReaderUtilitiesTest.java
index e2cd2e6..821df8a 100644
--- a/src/test/java/org/apache/commons/io/input/XmlStreamReaderUtilitiesTest.java
+++ b/src/test/java/org/apache/commons/io/input/XmlStreamReaderUtilitiesTest.java
@@ -31,7 +31,7 @@
 public class XmlStreamReaderUtilitiesTest {
 
     /** Mock {@link XmlStreamReader} implementation */
-    private static class MockXmlStreamReader extends XmlStreamReader {
+    private static final class MockXmlStreamReader extends XmlStreamReader {
         MockXmlStreamReader(final String defaultEncoding) throws IOException {
             super(CharSequenceInputStream.builder().setCharSequence("").get(), null, true, defaultEncoding);
         }
diff --git a/src/test/java/org/apache/commons/io/input/compatibility/XmlStreamReaderUtilitiesCompatibilityTest.java b/src/test/java/org/apache/commons/io/input/compatibility/XmlStreamReaderUtilitiesCompatibilityTest.java
index ee4310a..acfeb74 100644
--- a/src/test/java/org/apache/commons/io/input/compatibility/XmlStreamReaderUtilitiesCompatibilityTest.java
+++ b/src/test/java/org/apache/commons/io/input/compatibility/XmlStreamReaderUtilitiesCompatibilityTest.java
@@ -27,7 +27,7 @@
 public class XmlStreamReaderUtilitiesCompatibilityTest extends XmlStreamReaderUtilitiesTest {
 
     /** Mock {@link XmlStreamReader} implementation */
-    private static class MockXmlStreamReader extends XmlStreamReader {
+    private static final class MockXmlStreamReader extends XmlStreamReader {
         MockXmlStreamReader(final String defaultEncoding) throws IOException {
             super(CharSequenceInputStream.builder().setCharSequence("").get(), null, true, defaultEncoding);
         }
diff --git a/src/test/java/org/apache/commons/io/jmh/PathUtilsContentEqualsBenchmark.java b/src/test/java/org/apache/commons/io/jmh/PathUtilsContentEqualsBenchmark.java
new file mode 100644
index 0000000..20e49e7
--- /dev/null
+++ b/src/test/java/org/apache/commons/io/jmh/PathUtilsContentEqualsBenchmark.java
@@ -0,0 +1,119 @@
+/*
+ * 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.jmh;
+
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.RandomAccessFileMode;
+import org.apache.commons.io.RandomAccessFiles;
+import org.apache.commons.io.file.PathUtils;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Test different implementations of {@link PathUtils#fileContentEquals(Path, Path)}.
+ *
+ * <pre>
+ * Benchmark                                                                Mode  Cnt    Score   Error  Units
+ * PathUtilsContentEqualsBenchmark.testCurrent_fileContentEquals            avgt    5    4.538 ▒  1.010  ms/op
+ * PathUtilsContentEqualsBenchmark.testCurrent_fileContentEquals_Blackhole  avgt    5  110.627 ▒ 30.317  ms/op
+ * PathUtilsContentEqualsBenchmark.testProposal_contentEquals               avgt    5    1.812 ▒  0.634  ms/op
+ * PathUtilsContentEqualsBenchmark.testProposal_contentEquals_Blackhole     avgt    5   43.521 ▒  6.762  ms/op
+ * </pre>
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+@Warmup(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
+@Fork(value = 1, jvmArgs = { "-server" })
+public class PathUtilsContentEqualsBenchmark {
+
+    private static final Path bigFile1;
+    private static final Path bigFile2;
+
+    static {
+        // Set up test fixtures
+        try {
+            bigFile1 = Files.createTempFile(PathUtilsContentEqualsBenchmark.class.getSimpleName(), "-1.bin");
+            bigFile2 = Files.createTempFile(PathUtilsContentEqualsBenchmark.class.getSimpleName(), "-2.bin");
+            final int newLength = 1_000_000;
+            final byte[] bytes1 = new byte[newLength];
+            Arrays.fill(bytes1, (byte) 1);
+            Files.write(bigFile1, bytes1);
+            Files.copy(bigFile1, bigFile2, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    public static boolean newFileContentEquals(final Path path1, final Path path2) throws IOException {
+        try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(path1);
+                RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(path2)) {
+            return RandomAccessFiles.contentEquals(raf1, raf2);
+        }
+    }
+
+    @Benchmark
+    public boolean[] testCurrent_fileContentEquals() throws IOException {
+        final boolean[] res = new boolean[1];
+        res[0] = PathUtils.fileContentEquals(bigFile1, bigFile2);
+        return res;
+    }
+
+    @Benchmark
+    public void testCurrent_fileContentEquals_Blackhole(final Blackhole blackhole) throws IOException {
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                blackhole.consume(PathUtils.fileContentEquals(bigFile1, bigFile2));
+            }
+        }
+    }
+
+    @Benchmark
+    public boolean[] testProposal_contentEquals() throws IOException {
+        final boolean[] res = new boolean[1];
+        res[0] = newFileContentEquals(bigFile1, bigFile2);
+        return res;
+    }
+
+    @Benchmark
+    public void testProposal_contentEquals_Blackhole(final Blackhole blackhole) throws IOException {
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                blackhole.consume(newFileContentEquals(bigFile1, bigFile2));
+            }
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java
index 2d11e58..a65961d 100644
--- a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java
@@ -46,7 +46,7 @@
         T newInstance(final int size);
     }
 
-    private static class ByteArrayOutputStreamFactory implements BAOSFactory<ByteArrayOutputStream> {
+    private static final class ByteArrayOutputStreamFactory implements BAOSFactory<ByteArrayOutputStream> {
         @Override
         public ByteArrayOutputStream newInstance() {
             return new ByteArrayOutputStream();
@@ -58,7 +58,7 @@
         }
     }
 
-    private static class UnsynchronizedByteArrayOutputStreamFactory implements BAOSFactory<UnsynchronizedByteArrayOutputStream> {
+    private static final class UnsynchronizedByteArrayOutputStreamFactory implements BAOSFactory<UnsynchronizedByteArrayOutputStream> {
         @Override
         public UnsynchronizedByteArrayOutputStream newInstance() {
             return new UnsynchronizedByteArrayOutputStream();
diff --git a/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java
index dd1299e..080a492 100644
--- a/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java
+++ b/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java
@@ -33,7 +33,7 @@
 public class ThresholdingOutputStreamTest {
 
     @Test
-    public void testSetByteCount_Stream() throws Exception {
+    public void testSetByteCount_OutputStream() throws Exception {
         final AtomicBoolean reached = new AtomicBoolean(false);
         try (ThresholdingOutputStream tos = new ThresholdingOutputStream(3) {
             {
@@ -41,7 +41,7 @@
             }
 
             @Override
-            protected OutputStream getStream() throws IOException {
+            protected OutputStream getOutputStream() throws IOException {
                 return new ByteArrayOutputStream(4);
             }
 
@@ -58,7 +58,7 @@
     }
 
     @Test
-    public void testSetByteCount_OutputStream() throws Exception {
+    public void testSetByteCount_Stream() throws Exception {
         final AtomicBoolean reached = new AtomicBoolean(false);
         try (ThresholdingOutputStream tos = new ThresholdingOutputStream(3) {
             {
@@ -66,7 +66,7 @@
             }
 
             @Override
-            protected OutputStream getOutputStream() throws IOException {
+            protected OutputStream getStream() throws IOException {
                 return new ByteArrayOutputStream(4);
             }
 
diff --git a/src/test/java/org/apache/commons/io/test/TestUtils.java b/src/test/java/org/apache/commons/io/test/TestUtils.java
index 0620603..3a01f70 100644
--- a/src/test/java/org/apache/commons/io/test/TestUtils.java
+++ b/src/test/java/org/apache/commons/io/test/TestUtils.java
@@ -224,11 +224,11 @@
         }
     }
 
-    public static File newFile(final File testDirectory, final String filename) throws IOException {
-        final File destination = new File(testDirectory, filename);
+    public static File newFile(final File testDirectory, final String fileName) throws IOException {
+        final File destination = new File(testDirectory, fileName);
         /*
-        assertTrue( filename + "Test output data file shouldn't previously exist",
-                    !destination.exists() );
+        assertTrue(fileName + "Test output data file shouldn't previously exist",
+                    !destination.exists());
         */
         if (destination.exists()) {
             FileUtils.forceDelete(destination);
diff --git a/src/test/resources/org/apache/commons/io/test-file-empty2.bin b/src/test/resources/org/apache/commons/io/test-file-empty2.bin
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/test/resources/org/apache/commons/io/test-file-empty2.bin