Hi Anthony!
That's a quite common question about Java and FP, with a complex answer.
tl;dr:
Yes, I think you can criticize Java for not being "fully functional" and lacking features compared to other languages, but that's not the point of FP in Java, at least as I see it.
You wouldn't transition to Java only for its FP capabilities.
There are other languages that are "more functional", even on the JVM.
Scala is a great example of FP+OO in a single language, and Clojure is more of a "FP to the max" variant.
But that doesn't mean that a functional approach in Java is without merits.
Time for the longer answer...
Looking completely isolated at FP, comparing what's possible in languages other than Java, you can clearly see that Java isn't the best match.
Java might be a multi-paradigm language with a lot of functional and functional-akin additions, but it still isn't a "fully" functional language.
That's why I called the book "A Functional Approach to Java" instead of "Functional Programming with Java" or something similar.
For a greenfield project, we might start with the "perfect" language for a problem, but how often is that really the case?
Adding another language to an existing code base is possible, but we must consider the ramifications.
If you're the only developer on something, the decision might be up to you, but working in a team affects all of them.
Even though I would love to integrate Scala, Clojure, or Kotlin into my company's projects, my colleagues would have to deal with it, too.
Instead of forcing an FP language, we decided to stick with Java and do "a more functional approach" to certain topics instead.
Java's strengths are its general robustness, backward compatibility to a level that even hinders certain advancements, and a vast ecosystem.
Especially its backward compatibility means we have to deal with certain design decisions, even if they don't fit well or even hinder straightforward functional code.
Features like static types, lack of dynamic tuples, checked exceptions, nullability, or lacking tail-call optimization make Java "harder" than other languages for functional code and certain use-cases.
But which language is perfect for every use-case?
I think every language is missing some features, depending on how you look at it.
Most of the topics I discuss in the book are great for any paradigm.
Is it really important that Streams are declarative pipelines of higher-order functions?
Or that immutability, a cornerstone of functional programming, solves a lot of state-handling issues?
It's about opening up your mind to new possibilities, like approaching your problems with a functional mindset, even if the solution won't be as functional as it might be with another language.
As Brian Goetz, the Java Language Architect at Oracle, said in one of his talks:
Don’t be a functional programmer.
Don’t be an object-oriented programmer.
Be a better programmer.