Building UIs in Swing is this huge, festering gob of object instantiations and method calls. It's OOP at its absolute worst.
There are ways of making OOP smart, we have been talking about fluent interfaces, OO design patterns, AOP and higher level of abstractions similar to those of DSLs. But the real word is *productivity* and the language needs to make your user elegantly productive. Unfortunately in Java, we often find people generating reams of boilerplates (aka getters and setters) that look like pureplay copy-paste stuff. Java abstractions thrive on the evil loop of the 3 C's create-construct-call along with liberal litterings of getters and setters. You create a class, declare 5 read-write attributes and you have a pageful of code before you start throwing in a single piece of actual functionality. Object orientation procrastinates public attributes, restricts visibility of implementation details, but never prevents the language from providing elegant constructs to handle boilerplates. Ruby does this, and does it with elan.
Java is not Orthogonal
Paul Graham in On Lisp defines orthogonality of a language as follows :
An orthogonal language is one inwhich you can express a lot by combining a small number of operators in a lot of different ways.
He goes on to explain how the
complement
function in Lisp has got rid of half of the *if_not* funtions from the pairs like [remove-if
, remove-if-not
], [subst-if
, subst-if-not
] etc. Similarly in Ruby we can have the following orthogonal usage of the "*" operator across data types :"Seconds/day: #{24*60*60}"
will give Seconds/day: 86400"#{'Ho! '*3}Merry Christmas!"
will give Ho! Ho! Ho! Merry Christmas!C++ supports operator overloading, which is also a minimalistic way to extend your operator usage.
In order to bring some amount of orthogonality in Java we have lots of frameworks and libraries. This is yet another problem of dealing with an impoverished language - you have a proliferation of libraries and frameworks which add unnecessary layers in your codebase and tend to collapse under their weight.
Consider the following code in Java to find a matching sub-collection based on a predicate :
class Song {
private String name;
...
...
}
// ...
// ...
Collection<Song> songs = new ArrayList<Song>();
// ...
// populate songs
// ...
String title = ...;
Collection<Song> sub = new ArrayList<Song>();
for(Song song : songs) {
if (song.getName().equals(title)) {
sub.add(song);
}
}
The Jakarta Commons Collections framework adds orthogonality by defining abstractions like
Predicate
, Closure
, Transformer
etc., along with lots of helper methods like find()
, forAllDo()
, select()
that operate on them, which helps user do away with boilerplate iterators and for-loops. For the above example, the equivalent one will be :Collection sub = CollectionUtils.transformedCollection(songs,
TransformerUtils.invokerTransformer("getName"));
CollectionUtils.select(sub,
PredicateUtils.equalPredicate(title));
Yuck !! We have got rid of the for-loop, but at the expense of ugly ugly syntax, loads of statics and type-unsafety, for which we take pride in Java. Of course, in Ruby we can do this with much more elegance and lesser code :
@songs.find {|song| title == song.name }
and this same syntax and structure will work for all sorts of collections and arrays which can be iterated. This is orthogonality.
Another classic example of non-orthogonality in Java is the treatment of arrays as compared to other collections. You can initialize an array as :
String[] animals = new String[] {"elephant", "tiger", "cat", "dog"};
while for Collections you have to fall back to the ugliness of explicit method calls :
Collection<String> animals = new ArrayList<String>();
animals.add("elephant");
animals.add("tiger");
animals.add("cat");
animals.add("dog");
Besides arrays have always been a second class citizen in the Java OO land - they support covariant subtyping (which is unsafe, hence all runtime checks have to be done), cannot be subclassed and are not extensible unlike other collection classes. A classic example of non-orthogonality.
Initialization syntax ugliness and lack of literals syntax support has been one of the major failings of Java - Steve Yegge has documented it right to its last bit.
Java and Extensibility
Being an OO language, Java supports extension of classes through inheritance. But once you define a class, there is no scope of extensibility at runtime - you cannot define additional methods or properties. AOP has been in style, of late, and has proved quite effective as an extension tool for Java abstractions. But, once again it is NOT part of the language and hence does not go to enrich the Java language semantics. There is no meta-programming support which can make Java friendlier for DSL adoption. Look at this excellent example from this recent blogpost :
Creating some test data for building a tree, the Java way :
Tree a = new Tree("a");
Tree b = new Tree("b");
Tree c = new Tree("c");
a.addChild(b);
a.addChild(c);
Tree d = new Tree("d");
Tree e = new Tree("e");
b.addChild(d);
b.addchild(e);
Tree f = new Tree("f");
Tree g = new Tree("g");
Tree h = new Tree("h");
c.addChild(f);
c.addChild(g);
c.addChild(h);
and the Ruby way :
tree = a {
b { d e }
c { f g h }
}
It is really this simple - of course you have the meta-programming engine backing you for creating this DSL. What this implies is that, with Ruby you can extend the language to define your own DSL and make it usable for your specific problem at hand.
Java Needs More Syntactic Sugars
Any Turing complete programming language has the ability to allow programmers implement similar functionalities. Java is a Turing complete language, but still does not boost enough programmer's productivity. Brevity of the language is an important feature and modern day languages like Ruby and Scala offer a lot in that respect. Syntactic sugars are just as important in making programmers feel concise about the implementation. Over the last year or so, we have seen lots of syntactic sugars being added to C# in the forms of Anomymous Methods, Lambdas, Expression Trees and Extension Methods. I think Java is lagging behind a lot in this respect. The smart for-loop is an example in the right direction. But Sun will do the Java community a world of good in offering other syntactic sugars like automatic accessors, closures and lambdas.
Proliferation of Libraries
In order to combat Java's shortcomings at complexity management, over the last five years or so, we have seen the proliferation of a large number of libraries and frameworks, that claim to improve programmer's productivity. I gave an example above, which proves that there is no substitute for language elegance. These so called productivity enhancing tools are added layers on top of the language core and have been mostly delivered as generic ones which solve generic problems. There you are .. a definite case of Frameworkitis. Boy, I need to solve this particular problem - why should I incur the overhead of all the generic implementations. Think DSL, my language should allow me to carve out a domain specific solution using a domain specific language. This is where Paul Graham positions Lisp as a programmable programming language. I am not telling all Java libraries are crap, believe me, some of them really rocks,
java.util.concurrent
is one of the most significant value additions to Java ever and AOP is the closest approximation to meta-programming in Java. Still I feel many of them would not have been there, had Java been more extensible. Is it Really Static Typing ?
I have been thinking really hard about this issue of lack of programmer productivity with Java - is static typing the main issue ? Or the lack of meta-programming features and the ability that languages like Ruby and Lisp offer to treat code and data interchangeably. I think it is a combination of both the features - besides Java does not support first class functions, it doesn't have Closures as yet and does not have some of the other productivity tools like parallel assignment, multiple return values, user-defined operators, continuations etc. that make a programmer happy. Look at Scala today - it definitely has all of them, and also supports static typing as well.
In one of the enterprise Java projects that we are executing, the Maven repository has reams of third party jars (mostly open source) that claim to do a better job of complexity management. I know Ruby is not enterprise ready, Lisp never claimed to deliver performance in a typical enterprise business application, Java does the best under the current circumstances. And the strongest point of Java is the JVM, possibly the best under the Sun. Initiatives like Rhino integration, JRuby and Jython are definitely in the right direction - we all would love to see the JVM evolving as a friendly nest of the dynamic languages. The other day, I was listening to the Gilad Bracha session on "Dynamically Typed Languages on the Java Platform" delivered in Lang .NET 2006. He discussed about
invokedynamic
bytecode and hotswapping to be implemented in the near future on the JVM. Possibly this is the future of the Java computing platform - it's the JVM that holds more promise for the future than the core Java programming language.
23 comments:
Interesting article. I hasten to point out that language extensibility also has its dark side. C++ operator overload, which you mention, was infamous for making code unsupportable. I certainly saw plenty of code as a contract programmer in the 90's where you had to study the operator overloading for a week just to be able to read the code.
My favorite example of this nonsense was the example of using operator overloading to make C++ look like Pascal. Great fun.
i disagree with some of your points. one example, when was the last time you needed to hard code something like the tree example apart from unit tests? My take is this, "most of the time" Java can modularize many things nicely, if you think you are repeating yourself, re-factor, write a new method or a helper. it is many times better then introducing new language key words or meta-programming. and it is much more tool friendly (IDE's).
Nice article. There's a lot of things I love about Java, but expressivity is not one of them.
And it's not about the static typing. Lack of static typing is the one thing that annoys me in dynamic languages.
So what's nice about (for me) the dynamic languages is not the dynamism of types, but of code - the meta-programming, functional programming stuff. That's the stuff we need in java.
So no, it's not the static typing that's a problem. That's a good thing. I want to write less boilerplate iterating code, pass functions around with writing some interface so that I can create an anonymous inner class that implements it, have my getters and setters written automagically, etc. etc. etc.
Whomever you got your Java Tree class from did a fairly bad job at designing the API. Java can certainly do tight APIs:
Tree tree = tree("a",
tree("b", tree("d"), tree("e")),
tree("c", tree("f"), tree("g"), tree("h"))
);
... you just need to provide the helper method:
public Tree tree(String name, Tree... children) {
Tree tree = new Tree(name);
for(Tree child : children) tree.addChild(child);
return tree;
}
Similarly for your Collections example, you can use the concise Arrays.asList() method:
List animals = Arrays.asList("elephant", "tiger", "cat", "dog");
[for nameless_1] :
In Java refactoring does not help always to get rid of repetitions. A classic example is the ream of getters and setters - I agree they can be generated by IDEs. But at the end of the day they add up to the size of the jars, more code to maintain and this is definitely not DRY. And certainly brevity has not been a strong point of Java - Ruby and Scala do a much better job of it.
[for dietrich kappe] :
I agree that operator overloading may lead to unsupportable codebase, I personally have faced this music while maintaining C++ codebase for my client. I just wanted to mention that it offers some level of extension of operators - and given proper disciplined usage can lead to orthogonal syntaxes in a language. I remember in one of the Java One sessions long back, Gilad Bracha had talked about implementing operator overloading in Java, but only over his dead body .. :-)
[for jesse] :
Regarding Collection initialization example it is true that Java provides an alternative syntax based on Arrays.asList(). But I don't like the syntax loaded with statics and ultimately backed with an array in the implementation - it is not intuitive. I need to learn Arrays class to get an initialization syntax for a List. My point is that the elegance is not there in literal based initialization of Collections in Java. Take this example in Ruby :
sample_hashmap = {
"k1" : "val1",
"k2" : "val2",
"k3" : "val3",
...
}
Neat huh ! Java's syntax is much more verbose which becomes much worse in case of nested data structures.
"Building UIs in Swing is this huge, festering gob of object instantiations and method calls. It's OOP at its absolute worst."
Agreed, to some extent. When you've been working with Swing for a while you get to know which objects and method calls you really need, and how to organise things, and then you find that it's not really that bad. One approach is to wrap up the stuff that you find ugly, so that you keep the ugliness localised. It's a tradeoff.
"in Java, we often find people generating reams of boilerplates (aka getters and setters) that look like pureplay copy-paste stuff."
I don't know why people seem to hide variables, then just make them visible again through getters/setters. You may as well just make public variables. Sure, if this is in public APIs this makes it harder to refactor later, so only do it for internal code, or aim to annoy your clients.
"Object orientation ... never prevents the language from providing elegant constructs to handle boilerplates."
I don't think object orientation itself even requires the boilerplates, nevermind constructs to hide them. Encapsulation isn't about making fields appear as methods, it's more about making the class that contains the method not even appear to the API user (e.g., a private implementation class or even an anonymous class). Encapsulation can exist without access restrictions, even access restrictions enforced by code conventions only.
About Jakarta Commons Collections - your complaint is about an API that predates generics - nowadays you would be able to use static imports and you wouldn't sacrifice type safety.
"in Ruby we can do this with much more elegance and lesser code
@songs.find {|song| title == song.name }"
Fine, but this can be approached in Java:
find(songs,compose(equals(title),getName()));
This is the kind of Java code I like to write. Of course, it takes more setting up than Ruby's does, but that's ok.
"[arrays] cannot be subclassed and are not extensible" - True, but you can use them rather than extend them - anything you can do by subclassing you can emulate with composition - why would you want to subclass an array? To put extra methods on it, probably, in which case you may as well put those extras in a utility class, or in an object that holds an array. All you lose is direct assignment compatibility - an extra method call isn't too painful.
If Java had operator overloading, you could then overload the cast operator and make the assignment compatible.
About the Java code samples - try to write realistic code samples that demonstrate some working knowledge of Java (e.g., a commenter mentioned asList to shorten your collection initialisation code), and try not to use pre-1.5 type-unsafe libraries as examples, it only serves to lengthen your posts and make your points seem weaker, though some are good points.
Personally, I like the static typing, but feel that Java would benefit more from type inference. I don't know Scala, maybe it's similar, but use Haskell for a little while - at least follow Yet Another Haskell Tutorial.
You get a real functional language, with brief syntax, but compile errors that may as well be garbage (so far - I'm in the early stages of learning it).
I'm using some of the things I've learned from Haskell (and from bits of other languages such as Ruby) in Java, and it's fun.
[for ricky] :
some observations on your observations ..
[on getters and setters] :
Making all variables public is, of course, no solution. It exposes implementation details. In fact exposign implementation through getters is also never recommended. My main point was terseness and brevity, which has never been Java's forte. I think Ruby and Scala do this better.
[Jakarta Commons Collections] :
Yes, it predates generics, but unfortunately there is a huge codebase out there that use these unsafe api s. We have been using them in one of the projects which started before generics was there in Java. And lots of users still use them as time-tested api s.
[find(songs,compose(equals(title),getName()));] :
As I mentioned in my blog, Java is a turing complete language. Hence you can do everything which u can in other Turing complete languages. The question is elegance, intuitiveness and naturalness of syntax. Initializing a List with Arrays.asList() is never intuitive. Similarly the find/compose stuff that u have written needs lots of other plumbing to present to the user as a nice api. No wonder we have lots of frameworks in Java that try to hide the verbosity of the language. I have the same issue with arrays in Java as well. One other thing which I missed in my post is the complete disharmony in treating the primitives. The primitives in Java are a diff creature altogether and u need to write lots of specialised treatment codes to deal with primitives. But that's another story and deserves a separate post of its own.
The productivity issue with Java is overhyped, IMHO. Java is quite expressive for a wide range of problems, but is also, when well written, expressive in the human readable sense, in which verbosity can be a good thing.
One of the problems I've seen in a lot of Java code is the same problem in many other languages, when a programmer is not abstracting the problem domain, but rather trying to abstract the implementation.
Of course, to some extent that is unavoidable, and I've been guilty too, but code which expresses too much about patterns, factories, etc should be reworked to mostly hide those implementation artifacts, and rather express a view of the domain/problem space.
This is doable with or without OO,
but it is work.
One more thing, re: the comment about too much code for an object, because of all the getters & setters (and yes, this is a rant):
Getters and setters are evil. They miss the point about encapsulation and objects. Expose behavior, not data! If you are exposing mostly data, then rethink things. Sometimes that really is the nature of what you're doing, but mostly I've seen people blindly exposing data with getters and setters, and the unfortunate histroy of the old java bean spec doesn't help.
On top of asList(), you can also initialize collections as easily in Java as you can in Ruby:
List a = new ArrayList() {{
add("a");
add("b");
}};
If you're going to call something the "biggest failure of Java", at least make sure you know what you're talking about.
Anonymous: if getters and setters are evil, how am I supposed to express the fact that I need to get the name (a String) out of a Person object?
Map m = new HashMap() {{
put("k1", "val1");
put("k2", "val2");
put("k3","val3");
}};
You're welcome to find this more verbose than Ruby, but a lot of people will disagree.
[for Anonymous on Collection initialization:]
Thanks for pointing to one of those *lesser used* features of instance initialization. Yes, this is definitely more terse than the standard idiom for initialization. But, the main point of the blog post is to highlight the natural way of doing things and that Java adds more verbosity to it. I have never told that it is the "biggest failure in Java" - only that it may be less elegant than some of the practices in other programming languages.
The instance initialization syntax that u have pointed to *should never* be used in production code, since it has the overhead of an extra anonymous inner class. Hence it is not surprising that we do not come across such syntax frequently. Yes, we can use this in writing unit tests - and that is probably an area where hard coded initializations make more sense. But try using the same syntax to define more complex nested structures in Java .. possibly a Map of Lists .. I simply do not think this scales.
[for Anomymous: if getters and setters are evil, how am I supposed to express the fact that I need to get the name (a String) out of a Person object?]
Getters and setters are evil in the sense that in many cases careless exposure of getters and setters cause implementation to escape out of your model. So the focus should be *not* to expose implementation artifacts, but to publish *public* behavior of the abstraction.
Having used Perl and messed around with Python and Ruby I do agree that Java can be really verbose. This is one of the arguments I have with a colleague who is quick to point out that "IDEA will write it for you". My point is that code tends to be read for much more time than it is written and there is definitely a lot of value in being succint and expressive.
I find it strange how many people seem to think that succintness is synonymous with readability. I'd always find a 3-line for loop and call more readable than a 1-line perl map statement for example. Verbosity can be a problem, but I don't think it's one of the most important considerations in the readability of code.
I'm also less than convinced of the benefits of a fluent interface outside the realms of basic data construction. I've been using JMock practically every day for some time now and still find myself having to think carefully about the syntax I need. Perhaps it's just a bad example, but I find it consistently frustrating.
Interesting. I could write this same article changing the word "Ruby" to "Groovy" replace one or two symbols (changing the pipe "I" arg delimiter to an arrow "->") and acheive the same results. I could also compile my examples, package them as native Java APIs and sit them next to any existing Java code that needed them.
I am new here, so just a few comments on collection initialization:
You called the Arrays.asList() approach unintuitive - which it is, because you must first learn the extra helper method. Likewise the Ruby-way is 'unintuitive' since it employs some extra syntax (invented just for this purpose!) that you must learn before use.
The 'anonymous inner class trick' uses normal initialization syntax in a smart way to reduce typing work and text clutter. This is elegant and orthogonal (if it were easier to come up with this solution I would even call it intuitive, but... read on. ;-)
You criticize the overhead of the extra class but: where do we use such hard coded ad-hoc initialization? Mostly 'in writing unit tests' - like you said - or similar? Yes! And there the overhead would not matter much.
There's another reason, though, to mostly avoid this idiom: An anonymous or local class is always non-static, so the initialized list will keep a reference to the enclosing outer instance (and prevent it from being garbage collected as long as the list lives!) plus you cannot use it in static contexts without providing a dummy instance for the outer object (maybe stored as a static final, just to support that idiom: crazy, but at least then we wouldn't be too surprised that the dummy does not get garbage collected ;)
So this was not a good discussion example either. My opinion is that too much advocacy for concise syntax can be as bad as verboseness. And often verboseness can be as helpful as conciseness. Don't go overboard with "what if my language had that feature"-stories; mostly use it as is and be modest in your wishes - lest it gets overcrowded with obscure features that lengthen the learning curve - and the time required to understand your colleague's code of 'different style' that you must maintain since he 'suddenly left' last month (my 2c: the 'suddenly left'-syndrome is often due to a sudden disgust of a person towards their own coding style; and it probably happens with verbose as well as concise orthogonal languages ;-)
@Anonymous:
Good thoughts, well taken .. though at the end of the day, don't you think Java is a bit more verbose than what is needed to strike the right balance between conciseness and elegance ?
C# is the answer to everything, array to collection is fixed with List.AddRange(new string[] {"","",""});
However, I'm sad that by default Collections don't overload += and -= operators, that would be a very natural syntax.
Iterating over collections is fixed with easy to use anonymous delegates.
List.ForEach(delegate(object){ /*yay my action*/ });
And most user code runs faster than the equivalent java implimentation
hello,
I have to do 1 programm, can you check my programm
Create the abstract class Bird, there is no abstact method flapwings and String name.
Create derived abstract classes Flyingbird, Swimmingbird, Flyingandswimmingbird with corresponding abstact methods swim and/or fly.
Create no abstact subclasses from class Bird, for example Dove, Eagle, Ostrich, Swan.
Create 4 classes, which model next bird flocks:
1. flock of any birds
2. flock of flying birds
3. flock of swimming birds
4. flock of flying and swimming birds
Flocks create with the objects type Collection. There is espessial method for adding birds to the flock, it being known that adding alien objects must be impossible.
Classes structure must be so, that swan can adding to every flock. In every flock must be method, which make all birds, included in this flock, do, that all birds of flock ought do: flapwings and/or fly and/or swim.
About all actions print corresponding messages.
In test programm creat 4 different flocks, include in these flocks a few birds.
Class hierarchy must be such, that it would possible to make objects array, who can, for example to swim.
package bird;
import java.util.ArrayList;
import java.util.Iterator;
public abstract class Bird {
public String name;
public void strikewinds() {
System.out.println("Yes, relax, I'm bird and striking winds");
}
}
public abstract class Flyingbird extends Bird {
public abstract void fly();
}
public abstract class Swimmingbird extends Bird {
public abstract void swim();
}
public abstract class Flyswimbird extends Bird {
public abstract void flyswim();
}
public class Eagle extends Flyingbird {
public void fly(){
System.out.println("Yes, relax, I'm bird and fly");
}
}
public class Dove extends Flyingbird {
public void fly(){
System.out.println("Yes, relax, I'm bird and fly");
}
}
public class Penguin extends Swimmingbird {
public void swim(){
System.out.println("Yes, relax, I'm bird and swim");
}
}
public class Swan extends Flyswimbird {
public void flyswim(){
System.out.println("Yes, relax, I'm bird and fly and swim");
}
}
public class Ostrich extends Bird {
}
public class Chiken extends Bird {
}
public class Duck extends Flyswimbird {
public void flyswim(){
System.out.println("Yes, relax, I'm bird and fly and swim");
}
}
public class Arrays extends Flyingbird {
public static void main(String[] args) {
Flyingbird[] flyer = { new Eagle(), new Dove()};
for (int i = 0; i < flyer.length; i++)
flyer[i].fly();
}
}
class FlyingFlock {
private List Flyingbird birds = new ArrayList Flyingbird();
public void addBird(Flyingbird bird) {
birds.add(bird);
}
public void fly() {
}
}
class SwimmingFlock {
private List Swimmingbird birds = new ArrayList Swimmingbird();
public void addBird(Swimmingbird bird) {
birds.add(bird);
}
public void swim() {
}
}
class FlyswimFlock {
private List Flyswimbird birds = new ArrayList Flyswimbird();
public void addBird(Flyswimbird bird) {
birds.add(bird);
}
public void flyswim() {
}
}
class AnyFlock {
private List Bird birds = new ArrayList Bird();
public void addBird(Bird bird) {
birds.add(bird);
}
public void strikewinds() {
}
}
public class Test {
public static void main(String[] args) {
ArrayList Bird flock = new ArrayList Bird();
flock.add(new Eagle());
flock.add(new Penguin());
flock.add(new Swan());
Iterator i = flock.iterator();
while (i.hasNext()) {
try {
((Flyingbird)i.next()).fly();
((Swimmingbird)i.next()).swim();
((Flyswimbird)i.next()).flyswim();
}
catch (InterruptedException e) {
}
}
}
}
Post a Comment