Does simulation of closures in Java make sense?
Languages that have closures, such as Ruby, enable elegant constructs to transform lists. Suppose we have a class
class QueryTerm {
String value;
public String getValue() {...}
}
and a list of terms List<QueryTerm> terms
, that we want to transform into its list of values List<String> values
.
In Ruby we could write something like:
values1 = terms.collect do |term|
term.getValue()
end
Java forces us to construct the result list and iterate through the terms collection ourselves (at least there is no iterator or index position involved since foreach was introduced):
Collection<String> values2 = new HashSet<String>();
for (QueryTerm term : terms) {
values2.add(term.getValue());
}
Apache Commons and Google Collections2 try to emulate closures in Java by using anonymous classes:
Collection<String> values3 = Collections2.transform(terms, new Function<QueryTerm, String>() {
@Override
public String apply(QueryTerm term) {
return term.getValue();
}
});
The weird thing is, that this version has more lines of code and more characters than the original version! I would assume it is harder to understand for this reason. Therefore I dismissed the idea back when I saw it in Apache Commons. However, having seen it introduced in Google Collections recen开发者_C百科tly I am starting to wonder if I am missing something.
Therefore my question: What is your opinion of the constructs above? Do you consider the Collections2.transform()
version more readable/understandable than the default one? Am I missing something completely?
Best regards, Johannes
Closures as used here are pretty much the same thing as the Command pattern. I'm guessing any of the reasons commands can be useful apply here. A Command
could also be slightly more full fledged than a closure of course, since the latter is only on the function level. But a key observation is that this is often all you need.
There's a couple of advantages that are immediately obvious to me:
- Functional style of declaring your operations maps more easily to our way of thinking about them, i.e. here's my function, please apply it to this data.
- Chaining of transformations happens through transparent use of different calls to apply/filter/transform. You can just keep chaining them after each other and you see all the operations collected after each other in one line. Sure there's lots of boilerplate around it still, but if done right it's eminently readable to any java programmer.
Inversion of control: Say you want to decompose your operation over several different threads. If you wrote your own loop you're now stuck: you have to decompose it yourself into several different loops. This is usually ugly and tricky to write code, and it's going to require you to create several Runnables which you pass off to other threads. So really you're just working with closures all over again.
One of the JDK7 proposals that won't make it was apparently something that did exactly all this for you, where you just submit your data and define what filters to perform on it, and you let it go its merry way. You can actually download the code for this (package extra166y). All the ops you can see in the javadoc for this package could be eliminated and replaced by closures. So this is a good example why a certain approach fails because of lack of real closures, and "simulating" closures doesn't quite cut it anymore.
Really, there's tons of problems that you can really easily solve by just passing in a simple function that does what you want it to do to the data and let a framework fill in all the rest. You can do this right now with anonymous classes, but proper closures would at least cut away a lot of the boiler plate.
One of the frameworks I've used that uses something like this approach to cut away a lot of the boilerplate is Spring-Jdbc. Here you pass the JdbcTemplate a simple function that grabs your objects out of the ResultSet
and you pass it along with your query to template.query()
. This cuts out a lot of aggravation and the unreadability of the usual JDBC code. A definite win if you'll be maintaining anything like this.
IMO, even with more lines of code, the transform
version, in your particular example, looks more readable. Why? Because of this word, transform. You immediately know that a transformation is being performed in the collection whereas with the normal version you have to follow the flow of the code to understand what is the intent of it. Once you get the hang on these new constructs, you'll find them more readable.
I like them and use them alot. I believe the main benefit is not syntactic but rather that you can predefine and re-use the code that are declared above as anonymous classes. You could for example predefine standard transformations that you can pass to the transform function, even a NullTransform that does nothing to get a Null Object pattern effect. This allow you a more declarative style of coding.
What bruno conde said. Also, some IDEs (e.g. Eclipse) can hide anonymous classes, shrinking the Google Collection code to a single line. Now that is readable! :)
In software development being explicit and understandable (read: maintainable) is more important than terseness, unless you are having fun with throwaway code or perl script :-)
If what you want to achieve is to transform a list of objects to list of objects of another type your first Java example is the clearest with regards to the intention of the code, imho.
The Google collecions example also is explicitly clear on what the intention of the code is, although like Hannes said, I would not use anonymous classes but an explicitly defined class with a usefull name.
The syntactic sugar as you have in Ruby makes it easy to introduce transformations all over your code, and when they need to change it might be easy to miss one. Transforming lists isn't a very cheap function either and being able to use them easily comes with the with the risk of overuse.
It is very ugly, and people are yearning for better syntax. One can get used to it after a while, but it is intimidating for beginners.
Forget @Override - the code is already cluttered. @Override solves a problem that does not exist in reality, yet people are now using it as if it is a critical semantic element.
Functional programming is not a matter of many keystrokes you can save by using it, but mainly of how it can improve the readability of your code. For example by using lambdaj you can turn your code snippet:
Collection<String> values2 = new HashSet<String>();
for (QueryTerm term : terms) {
values2.add(term.getValue());
}
in this one:
Collection<String> values2 = extract(terms, on(QueryTerm.class).getValue());
Don't you think it is more readable in this way?
精彩评论