Skip to content

Commit dd93554

Browse files
committed
Improve performance of PathUtils.fileContentEquals(Path, Path)
- Add org.apache.commons.io.channels.FileChannels. - Add RandomAccessFiles#contentEquals(RandomAccessFile, RandomAccessFile). - Add RandomAccessFiles#reset(RandomAccessFile). - Add PathUtilsContentEqualsBenchmark.
1 parent 67bc02c commit dd93554

9 files changed

Lines changed: 400 additions & 21 deletions

File tree

pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<modelVersion>4.0.0</modelVersion>
2525
<groupId>commons-io</groupId>
2626
<artifactId>commons-io</artifactId>
27-
<version>2.14.1-SNAPSHOT</version>
27+
<version>2.15.0-SNAPSHOT</version>
2828
<name>Apache Commons IO</name>
2929

3030
<inceptionYear>2002</inceptionYear>
@@ -52,7 +52,7 @@ file comparators, endian transformation classes, and much more.
5252
<connection>scm:git:https://gitbox.apache.org/repos/asf/commons-io.git</connection>
5353
<developerConnection>scm:git:https://gitbox.apache.org/repos/asf/commons-io.git</developerConnection>
5454
<url>https://gitbox.apache.org/repos/asf?p=commons-io.git</url>
55-
<tag>rel/commons-io-2.13.0</tag>
55+
<tag>rel/commons-io-2.15.0</tag>
5656
</scm>
5757

5858
<developers>
@@ -294,8 +294,8 @@ file comparators, endian transformation classes, and much more.
294294
<commons.componentid>io</commons.componentid>
295295
<commons.module.name>org.apache.commons.io</commons.module.name>
296296
<commons.rc.version>RC1</commons.rc.version>
297-
<commons.bc.version>2.13.0</commons.bc.version>
298-
<commons.release.version>2.14.0</commons.release.version>
297+
<commons.bc.version>2.14.0</commons.bc.version>
298+
<commons.release.version>2.15.0</commons.release.version>
299299
<commons.release.desc>(requires Java 8)</commons.release.desc>
300300
<commons.jira.id>IO</commons.jira.id>
301301
<commons.jira.pid>12310477</commons.jira.pid>

src/changes/changes.xml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ The <action> type attribute can be add,update,fix,remove.
4747
</properties>
4848

4949
<body>
50-
<release version="2.14.1" date="202Y-MM-DD" description="Java 8 is required.">
50+
<release version="2.15.0" date="202Y-MM-DD" description="Java 8 is required.">
5151
<!-- FIX -->
5252
<action dev="sebb" type="fix" issue="IO-810" due-to="Laurence Gonsalves">
5353
XmlStreamReader encoding match RE is too strict.
@@ -73,7 +73,28 @@ The <action> type attribute can be add,update,fix,remove.
7373
<action dev="ggregory" type="fix" issue="IO-814" due-to="Elliotte Rusty Harold, Gary Gregory">
7474
Don't throw UncheckedIOException #491.
7575
</action>
76+
<action dev="ggregory" type="fix" issue="IO-814" due-to="Gary Gregory">
77+
RandomAccessFileMode.create(Path) provides a better NullPointerException message.
78+
</action>
79+
<action dev="ggregory" type="fix" due-to="Gary Gregory">
80+
Improve performance of PathUtils.fileContentEquals(Path, Path, LinkOption[], OpenOption[]) by about 60%, see PathUtilsContentEqualsBenchmark.
81+
</action>
82+
<action dev="ggregory" type="fix" due-to="Gary Gregory">
83+
Improve performance of PathUtils.fileContentEquals(Path, Path) by about 60%, see PathUtilsContentEqualsBenchmark.
84+
</action>
7685
<!-- ADD -->
86+
<action dev="ggregory" type="add" due-to="Gary Gregory">
87+
Add org.apache.commons.io.channels.FileChannels.
88+
</action>
89+
<action dev="ggregory" type="add" due-to="Gary Gregory">
90+
Add RandomAccessFiles#contentEquals(RandomAccessFile, RandomAccessFile).
91+
</action>
92+
<action dev="ggregory" type="add" due-to="Gary Gregory">
93+
Add RandomAccessFiles#reset(RandomAccessFile).
94+
</action>
95+
<action dev="ggregory" type="add" due-to="Gary Gregory">
96+
Add PathUtilsContentEqualsBenchmark.
97+
</action>
7798
<!-- UPDATE -->
7899
</release>
79100
<release version="2.14.0" date="2023-09-24" description="Java 8 is required.">

src/main/java/org/apache/commons/io/RandomAccessFiles.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,54 @@
1919

2020
import java.io.IOException;
2121
import java.io.RandomAccessFile;
22+
import java.nio.channels.FileChannel;
23+
import java.util.Objects;
24+
25+
import org.apache.commons.io.channels.FileChannels;
2226

2327
/**
24-
* Works on RandomAccessFile.
28+
* Works with {@link RandomAccessFile}.
2529
*
2630
* @since 2.13.0
2731
*/
2832
public class RandomAccessFiles {
2933

34+
/**
35+
* Tests if two RandomAccessFile contents are equal.
36+
*
37+
* @param raf1 A RandomAccessFile.
38+
* @param raf2 Another RandomAccessFile.
39+
* @return true if the contents of both RandomAccessFiles are equal, false otherwise.
40+
* @throws IOException if an I/O error occurs.
41+
* @since 2.15.0
42+
*/
43+
@SuppressWarnings("resource") // See comments
44+
public static boolean contentEquals(final RandomAccessFile raf1, final RandomAccessFile raf2) throws IOException {
45+
// Short-circuit test
46+
if (Objects.equals(raf1, raf2)) {
47+
return true;
48+
}
49+
// Short-circuit test
50+
final long length1 = length(raf1);
51+
final long length2 = length(raf2);
52+
if (length1 != length2) {
53+
return false;
54+
}
55+
if (length1 == 0 && length2 == 0) {
56+
return true;
57+
}
58+
// Dig in and to the work
59+
// We do not close FileChannels because that closes the owning RandomAccessFile.
60+
// Instead, the caller is assumed to manage the given RandomAccessFile objects.
61+
final FileChannel channel1 = raf1.getChannel();
62+
final FileChannel channel2 = raf2.getChannel();
63+
return FileChannels.contentEquals(channel1, channel2, IOUtils.DEFAULT_BUFFER_SIZE);
64+
}
65+
66+
private static long length(final RandomAccessFile raf) throws IOException {
67+
return raf != null ? raf.length() : 0;
68+
}
69+
3070
/**
3171
* Reads a byte array starting at "position" for "length" bytes.
3272
*
@@ -42,4 +82,17 @@ public static byte[] read(final RandomAccessFile input, final long position, fin
4282
return IOUtils.toByteArray(input::read, length);
4383
}
4484

85+
/**
86+
* Resets the given file to position 0.
87+
*
88+
* @param raf The RandomAccessFile to reset.
89+
* @return The given RandomAccessFile.
90+
* @throws IOException If {@code pos} is less than {@code 0} or if an I/O error occurs.
91+
* @since 2.15.0
92+
*/
93+
public static RandomAccessFile reset(final RandomAccessFile raf) throws IOException {
94+
raf.seek(0);
95+
return raf;
96+
}
97+
4598
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.io.channels;
19+
20+
import java.io.IOException;
21+
import java.nio.ByteBuffer;
22+
import java.nio.channels.FileChannel;
23+
import java.util.Objects;
24+
25+
import org.apache.commons.io.IOUtils;
26+
27+
/**
28+
* Works with {@link FileChannel}.
29+
*
30+
* @since 2.15.0
31+
*/
32+
public final class FileChannels {
33+
34+
/**
35+
* Don't instantiate.
36+
*/
37+
private FileChannels() {
38+
// no-op
39+
}
40+
41+
/**
42+
* Tests if two RandomAccessFiles contents are equal.
43+
*
44+
* @param channel1 A FileChannel.
45+
* @param channel2 Another FileChannel.
46+
* @param byteBufferSize The two internal buffer capacities, in bytes.
47+
* @return true if the contents of both RandomAccessFiles are equal, false otherwise.
48+
* @throws IOException if an I/O error occurs.
49+
*/
50+
public static boolean contentEquals(final FileChannel channel1, final FileChannel channel2, final int byteBufferSize) throws IOException {
51+
// Short-circuit test
52+
if (Objects.equals(channel1, channel2)) {
53+
return true;
54+
}
55+
// Short-circuit test
56+
final long size1 = size(channel1);
57+
final long size2 = size(channel2);
58+
if (size1 != size2) {
59+
return false;
60+
}
61+
if (size1 == 0 && size2 == 0) {
62+
return true;
63+
}
64+
// Dig in and do the work
65+
final ByteBuffer byteBuffer1 = ByteBuffer.allocateDirect(byteBufferSize);
66+
final ByteBuffer byteBuffer2 = ByteBuffer.allocateDirect(byteBufferSize);
67+
while (true) {
68+
final int read1 = channel1.read(byteBuffer1);
69+
final int read2 = channel2.read(byteBuffer2);
70+
if (read1 == IOUtils.EOF && read2 == IOUtils.EOF) {
71+
return byteBuffer1.equals(byteBuffer2);
72+
}
73+
if (read1 != read2) {
74+
return false;
75+
}
76+
if (!byteBuffer1.equals(byteBuffer2)) {
77+
return false;
78+
}
79+
byteBuffer1.clear();
80+
byteBuffer2.clear();
81+
}
82+
}
83+
84+
private static long size(final FileChannel channel) throws IOException {
85+
return channel != null ? channel.size() : 0;
86+
}
87+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* Provides classes to work with {@link java.nio.channels}.
20+
*
21+
* @since 2.15.0
22+
*/
23+
package org.apache.commons.io.channels;

src/main/java/org/apache/commons/io/file/PathUtils.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.IOException;
2222
import java.io.InputStream;
2323
import java.io.OutputStream;
24+
import java.io.RandomAccessFile;
2425
import java.math.BigInteger;
2526
import java.net.URI;
2627
import java.net.URISyntaxException;
@@ -69,7 +70,8 @@
6970
import org.apache.commons.io.Charsets;
7071
import org.apache.commons.io.FileUtils;
7172
import org.apache.commons.io.FilenameUtils;
72-
import org.apache.commons.io.IOUtils;
73+
import org.apache.commons.io.RandomAccessFileMode;
74+
import org.apache.commons.io.RandomAccessFiles;
7375
import org.apache.commons.io.ThreadUtils;
7476
import org.apache.commons.io.file.Counters.PathCounters;
7577
import org.apache.commons.io.file.attribute.FileTimes;
@@ -717,20 +719,20 @@ public static boolean fileContentEquals(final Path path1, final Path path2) thro
717719
/**
718720
* Compares the file contents of two Paths to determine if they are equal or not.
719721
* <p>
720-
* File content is accessed through {@link Files#newInputStream(Path,OpenOption...)}.
722+
* File content is accessed through {@link RandomAccessFileMode#create(Path)}.
721723
* </p>
722724
*
723725
* @param path1 the first stream.
724726
* @param path2 the second stream.
725727
* @param linkOptions options specifying how files are followed.
726-
* @param openOptions options specifying how files are opened.
728+
* @param openOptions ignored.
727729
* @return true if the content of the streams are equal or they both don't exist, false otherwise.
728730
* @throws NullPointerException if openOptions is null.
729731
* @throws IOException if an I/O error occurs.
730732
* @see org.apache.commons.io.FileUtils#contentEquals(java.io.File, java.io.File)
731733
*/
732734
public static boolean fileContentEquals(final Path path1, final Path path2, final LinkOption[] linkOptions, final OpenOption[] openOptions)
733-
throws IOException {
735+
throws IOException {
734736
if (path1 == null && path2 == null) {
735737
return true;
736738
}
@@ -764,9 +766,9 @@ public static boolean fileContentEquals(final Path path1, final Path path2, fina
764766
// same file
765767
return true;
766768
}
767-
try (InputStream inputStream1 = Files.newInputStream(nPath1, openOptions);
768-
InputStream inputStream2 = Files.newInputStream(nPath2, openOptions)) {
769-
return IOUtils.contentEquals(inputStream1, inputStream2);
769+
try (RandomAccessFile raf1 = RandomAccessFileMode.READ_ONLY.create(path1.toRealPath(linkOptions));
770+
RandomAccessFile raf2 = RandomAccessFileMode.READ_ONLY.create(path2.toRealPath(linkOptions))) {
771+
return RandomAccessFiles.contentEquals(raf1, raf2);
770772
}
771773
}
772774

0 commit comments

Comments
 (0)