Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Call a method on the return value of a method reference

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?

like image 995
fsperrle Avatar asked Apr 22 '16 16:04

fsperrle


3 Answers

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.

like image 124
Radiodef Avatar answered Nov 01 '22 06:11

Radiodef


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.

like image 22
Jay Anderson Avatar answered Nov 01 '22 06:11

Jay Anderson


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)));
}
like image 25
Hank D Avatar answered Nov 01 '22 04:11

Hank D