On the surface Go and Java might seem to have a lot in common. They are both modern C/C++ like languages with garbage collection, supporting object oriented programming.
But beyond that there are quite a lot of differences. I will not highlight so much of Java's strengths compared to Go, as Java has been around for a long time. What is more interesting is why a developer should want to choose Go over Java, given Java's ubiquity, large number of frameworks, tools etc.
First issue is efficiency both with respect to memory usage and performance. Go allows much more low level tuning similar to C. A problem in Java is that all types except primitive types are reference types. That means related data can't be stored in one location. E.g. say we have a Car object with 1 Engine object, 4 Wheel objects etc. All those objects are stored in different locations in memory. While in C or Go you could store all the Car related data as one continuous block of memory. Why is that important? In modern computers CPU's can process data a lot faster than it can be feed to it by regular RAM memory. Due to this frequently used parts of the main RAM memory are stored in a super fast memory called cache. For caches to be efficient related data needs to be close in address space. That is hard to achieve in Java.
An example of this in practice is the distributed version control system git. It is known to be very fast. It is written in C. A Java version JGit was made. It was considerably slower. Handling of memory and lack of unsigned types was some of the important reasons.
Shawn O. Pearce wrote on the git mailinglist:JGit struggles with not having an efficient way to represent a SHA-1. C can just say "unsigned char[20]" and have it inline into the container's memory allocation. A byte[20] in Java will cost an *additional* 16 bytes of memory, and be slower to access because the bytes themselves are in a different area of memory from the container object. We try to work around it by converting from a byte[20] to 5 ints, but that costs us machine instructions
Like C, Go does allow unsigned types and defining data structures containing other data structures as continuos blocks of memory
Method calls
Before reading this it might be good to read Scott Stanchfield's article on why all java method calls use pass-by-value, and that there is no such thing as pass-by-reference in Java. However as mentioned Java does not support value types for other than primitive types. This causes problems for method calls. One problem is that small objects like a Point might often be faster to copy in a method call, rather than copy their reference which is what Java does. More importantly perhaps is that value semantics is often easier understood. E.g. it would be natural to assume that Point would be a value type. If I pass a Point object to a function I don't expect my point to be modified by called function. And yet that can easily happen in Java.
Too strong focus on OOP
Since the decision was made that Java would have no free functions (even though static methods in a way is free functions) this has caused the Java designer to come up with very cumbersome syntax to deal with problem that would have been best handled with free functions. Instead of closures Java got nested classes:
myButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
frame.toFront();
}
});
The same thing is achieved a lot less verbosely in Go using closures:
myButton.addActionListener(func(e ActionEvent) {
frame.toFront();
});
Actually the Java version requires even more code, because the ActionListener class needs to be defined somewhere. The function object passed in the Go example is defined right were it is used.
Why Java code end up being considerably more verbose than Go code
When you start building more complicated things this problem starts adding up, causing excessive amounts of code to be needed in Java, while short readable code can be used in Go for the same thing. Consider this example. For a game engine I was writing I used Lua as a script language. Algorithms for planning movement of Non player characters was based on combining different functions describing different behavior. Without a lot of background information the code below is not easy to follow. But bare with me:
local seek = Geometry.makeSeek(player:position())
local combo = Geometry.combineBehavior({0.001,seek}, {1,flank})
flank, seek and combo are functions created by other functions makeFlank, makeSeek etc. The combo function is created by combining the flank and seek functions. Basically each function takes two arguments referred to as s0 and s1, which denotes a orientation and position in space. Below is the code that creates the seek function:
-- Goodness of trajectory from state 's0' to 's1'
function seek(s0, s1)
local p0 = s0:position()
local p1 = s1:position()
local d1 = s1:direction()
-- direction to target
local dir_target = (target-p1):unit()
-- direction of from current point to next on path
local dir_path = (p1-p0):unit()
return 0.25*(1 + d1*dir_target)*(1 + dir_path * dir_target)
end
return seek
end
The details of the code is not important. It is mainly vector operations used to calculate how desirable s1 is with respect to getting the non player character to the goal position target. The candidate s1 position and orientations are produced elsewhere by considering places in space which does not cause collision with other objects etc. The code below shows how we would do this in Java. Since Java does not have closures we have to use classes:
public float goodness(State s0, State s1);
public void add(Behavior b, float weight);
}
class Seek implements Behavior {
Vec2 target;
public Seek(Vec2 target) {
this.target = target;
}
public float goodness(State s0, State s1) {
Vec2 p0 = s0.position();
Vec2 p1 = s1.position();
Vec2 d1 = s1.direction();
// direction to target
Vec2 dir_target = (target.minus(p1)).unit();
// direction of from current point to next on path
Vec2 dir_path = (p1.minus(p0)).unit();
return 0.25*(1.0 + d1.dot(dir_target))*(1.0 + dir_path.dot(dir_target));
}
}
The different behaviors will then be combined as follows:
Behavior seek = new Seek(target);
Behavior combo = new Combo();
combo.add(flank, 1.0);
combo.add(seek, 0.001);
With Go we can write the code more like we did in the dynamic script language Lua.
func makeSeek(target Vec2) func(s0, s1 State) float {
func seek(s0, s1 State) float {
p0 := s0.position();
p1 := s1.position();
d1 := s1.direction();
// direction to target
dir_target := (target.minus(p1)).unit();
// direction of from current point to next on path
dir_path := (p1.minus(p0)).unit();
return 0.25*(1.0 + d1.dot(dir_target))*(1.0 + dir_path.dot(dir_target));
}
return seek;
}
The the different behavior can be combined in much the same way as it was combined in Lua. The last statement can be done in many ways. The code below is valid since Go supports functions with arbitrary number of arguments, but it can't be properly type checked at compile time.
flank := makeFlank(player, 10);
seek := makeSeek(target);
combo := makeCombo(flank, 1.0, seek, 0.001);
For stronger type checking one might want to do something like the code below, which requires defining a struct Elem with a function pointer and float member.
combo := makeCombo(List{Elem{flank, 1.0}, Elem{seek, 0.001}});
Doing something similar to the Java way is also possible:
var funcs FuncList;
funcs.Add(flank, 1.0);
funcs.Add(seek, 0.001);
combo := makeCombo(&funcs);
What should be apparent is how much boilerplate code one has to write when making Java programs:
- Every method needs public in front
- Type has to be repeated for every argument in method definition.
- An interface has to be defined every time we want to simulare combining two functions.
- The type of a variable has to be specified every time even though the compiler should be able to figure it out based on assignment
The lua code goes on to create new functions:
local larger = makeBinary(eval, greaterThan)
Since eval and larger have different function signatures, we would need to create two new interfaces in Java and two class implementing them. Each class will have two methods and some member variables. In contrast the Go solution would only require a total of 4 new functions. No classes or interfaces needs to be created.
Conclusion
As stated earlier the aim of this article was to make the case for Go over Java, so you have to excuse my bias. Java has improved a lot over the years. It used to be extremely verbose for simple things like reading and writing to console. Java 5 improved upon that a great deal. But as these examples show, Java still requires a lot of boilerplate code that doesn't add anything to readability and ability to abstract or reason about the problem you are trying to solve.
I have not touched upon other clear Go advantages like compile time, channels for easing concurrency programming etc. I wanted to show that even without Go's most touted features here is a lot to gain. As new versions of Java comes out new features are added which addresses deficiencies in the language. However the problem is that gradually Java loses some of its original appeal which was that it was a very simple language. Go starts with a small feature set than present Java, perhaps more comparable to the original Java. However the features are better selected so similar kind of expressive power as todays Java can be expressed with far fewer features. I think that is worth somthing
3 comments:
Nice article. I hope more people experiment with Go; I've certainly enjoyed the various little coding projects I've done with it.
Minor note: you can omit most of the end-of-line semicolons in your Go code, since they are inferred. This makes the visual comparison to Java even more striking. Also consider running the Go code through a syntax highlighter.
Object-oriented programming creates an illusion of more power by adding structure (removing flexibility) as we as humane cannot comprehend linear programming to it's full extent.
I do dislike Java, however your arguments are obscenely off-target. Java's purpose is not flexibility; is not to do things in less code. It accomplishes the opposite -- making it much easier to read TypicalBadProgrammer1's code in an enterprise of many many TypicalBadProgrammers.
Any statement you've made about how Java requires more code or that Go has a slightly nicer syntax is so worthless to say. Additionally your comment on ActionListener needing to be defined is false as Java does support absolute class paths.
Want a real argument against Java? It's a pile of garbage seemingly built on the foundation of a "can it be done?" experiment with additional garbage constantly be stapled on. Perhaps it's part of the 'beauty' of Java for the capacity to receive such stapled garbage. Regardless, Java doesn't come close to the potential staring it in the eyes. Many inconsistencies such as singular return types and ambiguous parameters begs the question "did anyone actually THINK about this?".
Java is great when it's programmer is a collective of people-who-hate-their-jobs-on-average. The resulting code is even comparably fast in such situations. Beyond that, it's as useless to the world now as the Saturn IB, and people should just learn to program already.
I just found out about Go.
It seems somewhat redundant not to mention no so feasible given the vast frameworks and utilities available in Java jars today.
Java is still considered a safe (Pointer wise) and flexible language.
Through annotations many technologies become instantly manageable.
(1)How does Go hope to compete with that. How can Go be so fast to develop except perhaps for the interpreter capabilities much like javascript?
(2) How convertable is C++ or Java to Go through automation including handling annotations in existing code?
(3) Is this at all possible?
Post a Comment