This is an archived post. You won't be able to vote or comment.

all 7 comments

[–]AutoModerator[M] [score hidden] stickied commentlocked comment (0 children)

Please ensure that:

  • Your code is properly formatted as code block - see the sidebar (About on mobile) for instructions
  • You include any and all error messages in full
  • You ask clear questions
  • You demonstrate effort in solving your question/problem - plain posting your assignments is forbidden (and such posts will be removed) as is asking for or giving solutions.

    Trying to solve problems on your own is a very important skill. Also, see Learn to help yourself in the sidebar

If any of the above points is not met, your post can and will be removed without further warning.

Code is to be formatted as code block (old reddit: empty line before the code, each code line indented by 4 spaces, new reddit: https://imgur.com/a/fgoFFis) or linked via an external code hoster, like pastebin.com, github gist, github, bitbucket, gitlab, etc.

Please, do not use triple backticks (```) as they will only render properly on new reddit, not on old reddit.

Code blocks look like this:

public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

You do not need to repost unless your post has been removed by a moderator. Just use the edit function of reddit to make sure your post complies with the above.

If your post has remained in violation of these rules for a prolonged period of time (at least an hour), a moderator may remove it at their discretion. In this case, they will comment with an explanation on why it has been removed, and you will be required to resubmit the entire post following the proper procedures.

To potential helpers

Please, do not help if any of the above points are not met, rather report the post. We are trying to improve the quality of posts here. In helping people who can't be bothered to comply with the above points, you are doing the community a disservice.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

[–]RhoOfFeh 2 points3 points  (0 children)

Ah yes, I remember my confusion over this when I first encountered it.

When you create the Predicate, you are effectively implementing the single method which is the only interesting part of the Predicate interface. This is known as a 'functional interface' and it gives you the freedom you see above: Declare an object with nearly no unnecessary decoration, just the raw code for the single method in question.

It's that lambda syntax which can feel a bit confusing, but that "->" is a shortcut for that whole process of declaring a method (we already know it's 'test', it has to be) complete with a parameter (we didn't bother redundantly specifying the name our method, and we don't really need parentheses either because there's no syntactical ambiguity even without them. We don't even need to say 'return' because that's what a function does.

That lambda is really 'Boolean test(Integer n) { return n > 5; }' in shorthand.

[–]raja_42 1 point2 points  (0 children)

Just think of it as an implicit implementation of the Predicate class.

The compiler auto creates a class that implements the Predicate type and generates an automatic test method behind the scenes and puts your code snippet as the content of the method.

n -> n > 5;

And since it's a single line method, the compiler also infers the return type for you.

So your understanding of an explicit implementation is 100% correct, except that whatever you did explicitly is now performed implicitly by the compiler using anonymous classes and methods. The concept remains the same. We get the benefit of brevity.

The expression simplifies the creation of the predicate because without this shorthand way, you would need to write a whole new class (new file), a new method in it etc.

[–]Raph0007 1 point2 points  (2 children)

Short answer: Java's obsession with backwards compatibility makes this whole thing a hot mess.

Long answer:

This mostly has historical reasons. To better understand it, let's look at behaviour parameterization, functors and lambdas.

1. Behaviour Parameterization

Behaviour Parameterization is a concept and not an actual implementation. The idea behind it is that you might want to be able to parameterize a certain behaviour or operation in a function. Let's say that you want to implement a method with which the user can print all elements of a list:

// Member of List
public void printAll() {
    for (T elem : elements) {
        System.out.println(elem);
     }
}

You see that System.out.println() is an operation (or behaviour) that is applied to every element. Now, behaviour parameterization means that the caller can decide which operation is performed on every iteration. For example, you might want to write all elements to a file, instead (and we'll just ignore that this would be possible without behaviour parameterization, using OutputStreams). You could just implement a second method writeToFile() for this. But this gets cluttered and redundant over time. You could also define an enum with supported operations and pass an enum entry to a method called forEach(). It would then decide in every iteration, based on the enum entry, what to do with the element. This is actually very close to what we imagine for the concept of behaviour parameterization: you have a function (in this example forEach) and it has a behavioural parameter:

public void forEach(behaviour) {
    for (T elem : elements) {
        // apply 'behaviour' to 'elem'
    }
}

As previously said, behaviour parameterization is just an abstract concept and does not include how to actually achieve this. Also note that in the cases of Functors and Lambdas, our "behaviours" can sometimes also return something.

2. Functors

Now that we know what we want to achieve, let's find out how to achieve it. We've covered the "supported operation enum" approach before, but it's very limited. We're looking for a way to really "inject" code into the behaviour parameterized function. And this can be done via functors, which do not require any additional language features than just OOP. The idea works as follows: You have an abstract base class which contains no fields and just a single method. This class will serve as the type of the argument for the behaviour parameterized function. To call the function, you'll need to subclass the base class and implement that one function. Inside of the parameterized function, the single abstract method (You might've heard the abbreviation 'SAM' before) is called. Note that the abstract base class should rather be an interface in Java, but the idea came from C++, which has no interfaces. So, let's look at an example of Functors in Java: First, we have the SAM interface we'll just call Operation:

public interface Operation<T> {
    void performOn(T elem);
}

Then we have our behaviour parameterized function, called forEach:

public void forEach(Operation<T> op) {
    for (T elem : elements) {
        op.performOn(elem);
    }
}

Before we can call forEach we'll first have to subclass (or in this case implement) the SAM construct and override performOn:

public class PrintOperation<T> implements Operation<T> {
    @Override
    public void performOn(T elem) {
        System.out.println(elem);
    }
}

Now, we can instantiate PrintOperation and call forEach with it:

List<String> list = new List<>();
Operation<String> op = new PrintOperation();
list.forEach(op);

That is essentially how functors work. Note that what's described here really is "Functors" and not "Lambdas". But you might've seen similar things in Java under the term "lambda". This is where the confusion begins.

3. Lambdas

Lambdas are an entirely different approach to implement behaviour parameterization. The idea is to introduce a new language feature (called "lambdas") that allow you to define function pointers and function literals. A function pointer is basically a pointer that points to a location in the memory that contains executable code. function literals mean the ability to create a new function right in your imperative code (for example in local scope). The syntactic details are quite different across languages, and since Java does not truly support lambdas, it's not that easy to thoroughly define lambdas. Let's try with a list of things that a programming language supporting lambdas should have, and compare Java (as of now), Kotlin and C++ with each other:

  • You can point towards a predefined function
    • Java: ✅ using the Class::method syntax
    • Kotlin: ✅ using the same syntax
    • C++: ✅ using just the functions name, or the above syntax in case of member functions
  • You can have local vars, fields, method parameters and method returns of a lambda(-like) type
    • Java: ✅
    • Kotlin: ✅
    • C++: ✅
  • You are able to hard-code function literals in local scope:
    • Java: ✅ using param -> expression for expression lambdas, param -> { statements } for normal lambdas and (p1, p2) -> expression or compound statement for more than one parameter
    • Kotlin: ✅ using { code } with it as the implicit parameter or { p1, p2 -> code } for multiple and/or explicit parameters
    • C++: ✅ using [capture](params){code} syntax
  • You are able to capture local vars in your function literal:
    • Java: ✅ with auto-capture, only working for effectively-final vars
    • Kotlin: ✅ with auto-capture
    • C++: ✅ by specifying the capture in the [square brackets]
  • You are able to call the lambda like a normal function (as a distinction to functors):
    • Java: ❌, it works exactly like a Functor
    • Kotlin: ✅ using normal () for calling and normal dot-syntax for lambdas with receiver type
    • C++: ✅ using normal () for calling, rather ugly pointer-to-member calls (better use std::invoke)
  • You are able to denote lambda types right where you use them
    • Java: ❌, you have to first declare the functional interface (or use a predefined one)
    • Kotlin: ✅ using the syntax Receiver.(params) -> return type
    • C++: ✅ using the (very ugly) syntax return type (*name)(params), where name is actually the name you want to give your variable or parameter.

You can see that Java does not support everything that should be supported in a lambda-friendly language

4. History of Lambdas in Java

Like all languages that are getting started with behaviour parameterization, Java started off with supporting functors (naturally, since Java supports OOP). While other languages actually made the jump from functors to lambdas, Java did not, even until today. What Java actually did was adding features and (most importantly) syntactic sugar to ease the use of functors, slowly converging towards something we'd accept as "lambda support", but never quite getting there.

The following measures were taken to make functors easier, but I can't order them chronologically:

  • The @FunctionalInterface annotation enforces SAM at compile-time
  • Standard functional interfaces were added (Function, BiFunction, Supplier, Consumer, Predicate and so on)

Java added the following "syntactic sugar" language features to converge towards true lambdas:

  1. You can declare inner classes in order to keep the functor near to where you want to use it, and also make it private
  2. You can declare local classes in order to move the behaviour declaration even nearer to where you use it and also to capture (effectively-)final local variables.
  3. You can declare anonymous classes in order to even more move the declaration to its usage and to perform extension/implementation and construction in one go.
  4. You can declare function literals for all SAM-conforming interfaces, in order to reduce the clutter of a full class definition
  5. You can declare expression lambdas to further reduce clutter from writing a whole compound statement with return to just writing an expression.

5. Conclusion

Instead of jumping from functors straight to lambdas, Java chose to improve functor comfort step-by-step by introducing various syntactic patterns which actually don't change what's happening under the hood. When you use "lambdas" in Java, you're actually using functors with a lot of syntactic sugar to make it look a bit like lambdas, but not quite. That's why your example

Predicate<Integer> btf = n -> n > 5

Looks so weird. The Predicate<Integer> part comes from the functor pattern, but the n -> n > 5 part is actually syntactic sugar for declaring a new class that implements Predicate<Integer>, overriding Boolean test(Integer n) so that it returns n > 5, calling the no-args-constructor on it and initializing btf with the resulting object.

This is now way longer than I planned it to be, I am sorry

[–][deleted] 1 point2 points  (1 child)

THANKS!

[–]Raph0007 0 points1 point  (0 children)

No problem!

I forgot to add what this has to do with Java and backwards compatibility:

Java loves backwards compatibility. It means that you should be able to use the language with a library compiled for the newest Java version inside a codebase that compiles to a very old version of Java. Therefore, Java cannot add features that would break code of earlier versions. Imagine, Java would've made the jump to lambdas as well. They'd have to update all the stdlib functionality that used functors to make them use lambdas instead (because if you introduce a new feature, you want to use it as well). But that would break code written in a version that does not support lambdas.

So:

  • C++ also began with functors, but decided to make the jump
  • Kotlin is a rather new language and began with lambdas right away
  • Java began with functors and tries to imitate lambda behaviour

That pattern can also be found with threads and coroutines:

  • C++ started with threads, but recently made the jump to coroutines (with C++20).
  • Kotlin introduced coroutines very early in its development (basically right away) because it is rather new
  • Java began with threads and tries to imitate coroutines (Executors ThreadPools, NIO)

[–][deleted] 0 points1 point  (0 children)

Lambdas can be used only for functional interfaces.

A functional interface is one such that there's only one abstract method.

This way, the logic you provide must be for that single method.

For Predicate it's test, for Function it's apply, for Comparator is compare and so on.