'How to get rid of this multiple comparator methods in Java?
I want to make the comparator for file object as parameterized. (Ex: sort by name, length, size, last modified, file extension). One method with parameterized as sortType.
I don't want different methods as stated in Sort Array by Date and Size (Files)
private void sortByName(File[] files){
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File t, File t1) {
return t.getName().compareTo(t1.getName());
}
});
}
private void sortByDate(File[] files){
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File t, File t1) {
return (int) (t.lastModified() - t1.lastModified());
}
});
}
private void sortBySize(File[] files){
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File t, File t1) {
return (int) (t.length() - t1.length());
}
});
}
How to create a method something like sort(String sortType) or sort(Enum sortType) and have only one sorting comparator to do the needful.
Having multiple methods is having maintenance issue and unnecessary boilerplate code.
Thanks.
Solution 1:[1]
With lambdas, method references here, and the comparing methods of Comparator, one can write:
Arrays.sort(listOfFiles, Comparator.comparing(File::getName));
Arrays.sort(listOfFiles, Comparator.comparingLong(File::lastModified));
Arrays.sort(listOfFiles, Comparator.comparingLong(File::length));
This even allows composition of comparators, like reverse order, or first by last modified, then length or such.
Arrays.sort(listOfFiles, Comparator.comparingLong(File::length).reversed());
Arrays.sort(listOfFiles, Comparator.comparingLong(File::lastModified)
.thenComparingLong(File::length));
For an internal usage this suffices. For an external library API maybe not.
Sorting by file extension:
static final Comparator<File> BY_EXTENSION =
Comparator.comparing(f -> f.getName().replaceFirst("^(.*?)((\\.[^\\.]*)?)$", "$2"));
Arrays.sort(listOfFiles, BY_EXTENSION);
The lambda passed to sort does not need to be a method reference like File::getName but can be some function on File.
The replaceFirst should only keep the extension, yielding ".txt" or "" (no dot in name).
Or if you need the file extension as function:
class Foo { // Need some class name.
public static String getFileExtension(File file) {
return file.getName()
.replaceFirst("^(.*?)((\\.[^\\.]*)?)$", "$2");
}
}
Arrays.sort(Foo::getFileExtension);
Again using a method reference, though of a static method, File as parameter instead of as this.
Should one use a Comparator<Path> instead of Comparator<File> - as Path is more general than File -, then be aware that Path.getName() returns a Path:
static final Comparator<Path> BY_EXTENSION =
Comparator.comparing(p -> p.getName().toString()
.replaceFirst("^(.*?)((\\.[^\\.]*)?)$", "$2"));
In one method:
public static final Comparator<Path> BY_NAME =
Comparator.comparing(File::getName);
public static final Comparator<Path> BY_MODIFIED =
Comparator.comparingLong(File::lastModified);
public static final Comparator<Path> BY_LENGTH =
Comparator.comparingLong(File::length);
public static final Comparator<Path> BY_EXTENSION =
Comparator.comparing(p -> p.getName().toString()
.replaceFirst("^(.*?)((\\.[^\\.]*)?)$", "$2"));
Either
Arrays.sort(listOfFiles, BY_NAME);
or:
void sortFiles(File[] files, Comparator<File> comparator) {
Arrays.sort(files, comparator);
}
sortFiles(listOfFiles, BY_NAME);
With Stream:
List<File> sortFiles(Stream<File> fileStream, Comparator<File> comparator) {
return fileStream.sorted(comparator).collect(Collectors.toList());
}
File[] fileArray = ...
List<File> fileList = ...
List<File> sorted = sorted(Arrays.stream(fileArray), BY_NAME);
List<File> sorted = sorted(fileList.stream(), BY_NAME);
Solution 2:[2]
You can create an enum that implements Comparator<File>, and have each enum type to implement the compare method
enum FileComparator implements Comparator<File> {
ByName {
@Override
public int compare(File f1, File f2) {
return f1.getName().compareTo(f2.getName());
}
},
ByDate {
@Override
public int compare(File f1, File f2) {
return (int) (f1.lastModified() - f2.lastModified());
}
},
BySize {
@Override
public int compare(File f1, File f2) {
return (int) (f1.length() - f2.length());
}
};
}
Below code is the implementation to test
public class Lab {
public static void main(String[] args) {
File folder = new File("/Users/**/Documents");
File[] listOfFiles = folder.listFiles();
Arrays.sort(listOfFiles,FileComparator.ByDate);
System.out.println(Arrays.toString(listOfFiles));
}
}
Solution 3:[3]
If I understand you correctly, you want the code look more concise with just one Comparator. Now with Lambda and Java 13's enhanced switch, you can write sortBy as follows:
enum SortType {
Name, Date, Size
}
void sortBy(SortType type, File[] files) {
Arrays.sort(files, (t, t1) -> switch (type) {
case Name -> t.getName().compareTo(t1.getName());
case Date -> Long.compare(t.lastModified(), t1.lastModified());
case Size -> Long.compare(t.length(), t1.length());
});
}
But from the view of performance, it would be better to use three independent comparators.
Without lambda and enhanced switch, sortBy is written as:
void sortBy(SortType type, File[] files) {
Arrays.sort(files, new Comparator<File>() {
@Override
public int compare(File t, File t1) {
switch (type) {
case Name:
return t.getName().compareTo(t1.getName());
case Date:
return Long.compare(t.lastModified(), t1.lastModified());
case Size:
return Long.compare(t.length(), t1.length());
};
throw new IllegalStateException("Unreachable!");
}
});
}
Solution 4:[4]
It's easiest to use a Stream:
File[] files = {};
Arrays
.stream(files)
.sorted(
Comparator
.comparing(File::getName)
.thenComparing(File::lastModified)
.thenComparing(File::length)
)
.collect(Collectors.toList());
Solution 5:[5]
You can try with this method, you don't need to create enum
Doing it with strings would be the simplest.
public static List shortFiles(String shortType, List files){
files.sort(new Comparator<File>() {
@Override
public int compare(File t, File t1) {
int x=0;
switch (shortType) {
case "name" ->{x = t.getName().compareTo(t1.getName());}
case "date" ->{x = Long.compare(t.lastModified(), t1.lastModified());}
case "size" ->{x = Long.compare(t.length(), t1.length());}
}
return x;
}
});
return files;
}
Additional to that i have created a filter for the file List
/* I create a FileFilter for files with a extension diferent of *.tmp or *.TMP */
FileFilter logFilefilter = new FileFilter() {
@Override
public boolean accept(File file) {
return !file.getName().toLowerCase().endsWith(".tmp") && file.getName().contains(".")&& file.isFile();
}
};
Then create the file List with the File Filter (optional) specifying the path for the files
/* Specify the Directory which contains the files to short */
File f = new File("C:\\Users\\brdn\\AppData\\Local\\Temp");
if(f.exists()){//execute if the Directory exists
/* create the files with the FileFilter (optionally) */
List<File> files = Arrays.asList(f.listFiles(logFilefilter));
}else
System.out.println("The directory: " + f.getAbsolutePath() + " does not exist");
Finally here you a complete example
package brdn_company.mavenproject2;
import java.io.File;
import java.io.FileFilter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.text.SimpleDateFormat;
public class Mavenproject2 {
public static void main(String[] args) {
/* I create a FileFilter for files with a extension diferent of *.tmp or *.TMP */
FileFilter logFilefilter = new FileFilter() {
@Override
public boolean accept(File file) {
return !file.getName().toLowerCase().endsWith(".tmp") && file.getName().contains(".")&& file.isFile();
}
};
/* Specify the Directory which contains the files to short */
File f = new File("C:\\Users\\brdn\\AppData\\Local\\Temp");
if(f.exists()){//execute if the Directory exists
/* create the files with the FileFilter (optionally) */
List<File> files = Arrays.asList(f.listFiles(logFilefilter));
System.out.println("----------------SHORTING BY NAME----------------");
files = shortFiles("name", files);
for(File file : files)
System.out.println(file.getName());
System.out.println("\n----------------SHORTING BY SIZE----------------");
files = shortFiles("size", files);
for(File file : files)
System.out.format("%-70s size= %d bytes \n",file.getName(), file.length());
System.out.println("\n----------------SHORTING BY DATE----------------");
files = shortFiles("date", files);
for(File file : files)
System.out.format("%-70s Date= %s \n", file.getName(), new SimpleDateFormat("dd'/'MMMM'/'YYYY").format(new Date(file.lastModified())) );
}else
System.out.println("The directory: " + f.getAbsolutePath() + " does not exist");
}
public static List shortFiles(String shortType, List files){
files.sort(new Comparator<File>() {
@Override
public int compare(File t, File t1) {
int x=0;
switch (shortType) {
case "name" ->{x = t.getName().compareTo(t1.getName());}
case "date" ->{x = Long.compare(t.lastModified(), t1.lastModified());}
case "size" ->{x = Long.compare(t.length(), t1.length());
case "extension" ->{
x = t.getName().contains(".")==t1.getName().contains(".")? t.getName().split("[.]")[1].compareTo(t.getName().split("[.]")[1])
: !t.getName().contains(".")?1:
-1;}}
}
return x;
}
});
return files;
}
}
Which outputs:
----------------SHORTING BY NAME----------------
.ses
DTL-5996-162487191930034982.fxml
DTL-5996-3330347463011941505.fxml
DTL-5996-3504539881087568310.fxml
JavaDeployReg.log
JavaLauncher.log
fallback4142601253054055938.netbeans.pom
fallback8710927843535716668.netbeans.pom
jusched.log
pcsx2-1.6.0-include_standard.exe
sqlite-3.34.0-ffd2f809-64f1-445e-85ce-61dea205401e-sqlitejdbc.dll
----------------SHORTING BY SIZE----------------
.ses size= 53 bytes
fallback4142601253054055938.netbeans.pom size= 203 bytes
fallback8710927843535716668.netbeans.pom size= 207 bytes
JavaDeployReg.log size= 1943 bytes
DTL-5996-162487191930034982.fxml size= 4248 bytes
DTL-5996-3504539881087568310.fxml size= 4248 bytes
DTL-5996-3330347463011941505.fxml size= 4298 bytes
JavaLauncher.log size= 12978 bytes
pcsx2-1.6.0-include_standard.exe size= 137428 bytes
jusched.log size= 623658 bytes
sqlite-3.34.0-ffd2f809-64f1-445e-85ce-61dea205401e-sqlitejdbc.dll size= 852992 bytes
----------------SHORTING BY DATE----------------
pcsx2-1.6.0-include_standard.exe Date= 06/mayo/2020
sqlite-3.34.0-ffd2f809-64f1-445e-85ce-61dea205401e-sqlitejdbc.dll Date= 13/abril/2022
JavaDeployReg.log Date= 15/abril/2022
jusched.log Date= 15/abril/2022
JavaLauncher.log Date= 17/abril/2022
fallback8710927843535716668.netbeans.pom Date= 17/abril/2022
fallback4142601253054055938.netbeans.pom Date= 17/abril/2022
DTL-5996-162487191930034982.fxml Date= 17/abril/2022
DTL-5996-3504539881087568310.fxml Date= 17/abril/2022
DTL-5996-3330347463011941505.fxml Date= 17/abril/2022
.ses Date= 19/abril/2022
Good luck :) and sorry for the grammar, I'm a Spanish speaker.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | user18858988 |
| Solution 5 |
