Call a method on the return value of a method refe

2019-04-08 11:29发布

问题:

I have a stream of files that I want to filter based on the ending of the file name:

public Stream<File> getFiles(String ending) throws IOException {
    return Files.walk(this.path)
            .filter(Files::isRegularFile)
            .map(Path::toFile)
            .filter(file -> file.getName().endsWith(ending));
}

While the lambda in the last line is not bad, I thought I could use method references there as well, like so:

 .filter(File::getName.endsWith(ending));

Or alternatively wrapped in parentheses. However, this fails with The target type of this expression must be a functional interface

Can you explain why this doesn't work?

回答1:

Can you explain why this doesn't work?

Method references are syntactical sugar for a lambda expression. For example, the method reference File::getName is the same as (File f) -> f.getName().

Lambda expressions are "method literals" for defining the implementation of a functional interface, such as Function, Predicate, Supplier, etc.

For the compiler to know what interface you are implementing, the lambda or method reference must have a target type:

// either assigned to a variable with =
Function<File, String> f = File::getName;
// or assigned to a method parameter by passing as an argument
// (the parameter to 'map' is a Function)
...stream().map(File::getName)...

or (unusually) cast to something:

((Function<File, String>) File::getName)

Assignment context, method invocation context, and cast context can all provide target types for lambdas or method references. (In all 3 of the above cases, the target type is Function<File, String>.)

What the compiler is telling you is that your method reference does not have a target type, so it doesn't know what to do with it.



回答2:

File::getName is a method reference and String::endsWith is as well. However they cannot be chained together. You could create another method to do this

public static Predicate<File> fileEndsWith(final String ending) {
    return file -> file.getName().endsWith(ending);
}

and then use it

.filter(MyClass.fileEndsWith(ending))

This doesn't buy you much if you're not re-using it though.



回答3:

A couple of helpers might assist in providing some semblance of what you wish for. Using the helpers below, you can replace your lambda with an expression containing method references, like this:

// since your predicate is on the result of a function call, use this to create a predicate on the result of a function
public static <A,B> Predicate<A> onResult(Function<A,B> extractor, Predicate<B> predicate){
    return a -> predicate.test(extractor.apply(a));
}

// since your predicate involves an added parameter, use this to reduce the BiPredicate to a Predicate with one less parameter
public static <T,U> Predicate<T> withParam(BiPredicate<T,U> pred, U param){
    return t -> pred.test(t,param);
}

public Stream<File> getFiles(String ending) throws IOException {
    return Files.walk(Paths.get("."))
            .filter(Files::isRegularFile)
            .map(Path::toFile)
            .filter(onResult(File::getName, withParam(String::endsWith, ending)));
}