80% boilerplate is a large exaggeration.
80% boilerplate is a large exaggeration.
Culture is really important in programming languages and I'm not sure it is something you can ever separate from the language itself, as for example the standard library dictates in large part the structure of your own code.
I do agree though that there is no great reason Java has to be more verbose or more confusing than say Go.
Not a Go developer, but two things off the top of my head:
1. Do not need to explicitly declare interfaces a struct conforms to.
2. Do not need to declare types of variables inferred on initialization.
3. Syntax for slice, map and struct literals.
I'm sure there's more...
Can't have nice syntax for object literals, maps, slices, etc, though :( I mean, I have no hope for it to be introduced to the language any time soon, even if the desire is there among the language stewards.
`var` is part of standard Java, you don't have to use Lombok.
> Can't have nice syntax for object literals
Could you elaborate on what you mean by object literals in this context?
const foo = new Bar
a = 1,
b = 2
}
With a default constructor or a builder (both provided by Lombok, maybe with other libraries, too) this level of succinctness is quite achievable in Java, too.Which comes with its own (sometimes dangerous) downsides.
> 2. Do not need to declare types of variables inferred on initialization.
Java has `var` for quite some time now:
var foo = new Foo<Bar>();
is valid.> Syntax for slice, map and struct literals
I assume you mean overloading `[]` for slices and maps. This is a nice feature (C# and Kotlin already do this). But it doesn't reduce boilerplate by much in practice.
I don't buy this. I've heard the theoretical arguments to the contrary, but I've never (in 7 years of consistent Go use) heard of anyone being bit by this in practice--certainly not to any significant consequence. On the other hand, implicit interfaces materially benefit nearly every project in the Go ecosystem. I would make this tradeoff all day every day.
Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
> Secondly, I found that in practice, it doesn't buy you much, and on the contrary, makes working with code more difficult (even with an IDE).
I don't know about you, but I don't like having to write boilerplate to make a third party class implement a first party interface.
> It's valuable information to know what interface(s) a type implements, not to mention finding all types down the hierarchy that implement a given interface (much more tedious and resource hungry with golang).
How is that information valuable? The only thing I can think of is "I want to change the interface and consequently I need to update the implementations". In which case, just change the interface and the compiler will tell you what broke. As for "resource hungry", who cares? I'll trade 50ms of computing time per search (note that this is a generous figure) if it means developers don't need to maintain "implements" annotations and the corresponding boilerplate discussed above.
It's useful when you want to know where a certain concrete type is being passed as an interface. If you have a type Foo that has functions bar() and baz(), then it becomes very tedious to see where Foo is being used as a `Barrer` (in golang style), `Bazzer` or even `BarrerBazzer` because it now automatically implement all three interfaces.
Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
> I'll trade 50ms of computing time per search (note that this is a generous figure)
On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
That's just a generalization of the example I gave, and the same solution applies (although there are probably automated solutions). Unless there are other special cases that you have to do so frequently that the solution previously mentioned is too tedious, then I don't see what the fuss is about.
> Another use case is that it becomes awkward to define "tag" interfaces (look at Rust's `Send` trait for instance). Now you're forced to define an interface with a function `isSend` or something and hope that no one else declares another interface with the same signature and passes in types that implement that. Again, more bug-prone behavior.
I don't know how to take this comment seriously. The odds of someone implementing isSend and passing it to the wrong interface have to be astronomically low. The frequency of such bugs must be approaching zero. It's patently unreasonable to consider this to be "bug prone", moreover, if you're really, really concerned about it, randomly generate your private method name to whatever degree of entropy you prefer.
> On any non-trivially sized project, it takes way longer than that to look up implementations of interfaces in the IDEs I've used so far. You're looking at 30s+ sometimes.
File those bugs with the IDEs. Any IDE worth its salt holds the set of types in memory. Even if the set is just a list/array (as opposed to some data structure that is indexed by the interfaces they implement), it should take no time at all to filter that to the set that implements the interface, even if there are tens of thousands of types.
Well, it was only added in the latest LTS release, which was released a little over a year ago. (Or a few months before that if you used the non-LTS Java 10.)
It's about time – C# had it since 2007.
The things you mention don’t bother me as much as pointless ceremony like getters, factories, and insane object hierarchies, all of which are really optional. I quite like Go and use it a lot, but it is not IMO radically different from Java in syntax, except perhaps in what it leaves out (inheritance, exceptions, explicit declarations), and its culture of radical simplicity.
Does Go have objects with functions? If so, it has getters and setters, Go programmers just choose not to use them. I could absolutly write Python code like this:
class MyObject(object):
def set_foo(this, foo):
this.foo = foo
def get_foo(this):
return this.foo
The thing is...Java doesn't have getters and setters. There's nothing about the language that requires them. This is perfectly legal Java code: class MyObject {
public int foo;
}
Look at it! It's a public member! You can read/write to it without a getter/setter! You know, something every language has, but for some reason, it's considered verboten in Java.Java is a perfectly fine language. It's Java programmers that are awful for sticking to these unnecessary paradigms that necessitate ridiculous amounts of boilerplate.
Java programmers are a source of frustration for me. I don't write Java professionally, but I do application security which involves reading a lot of Java code. And because the developers are sticking so strictly to the typical Java paradigms, the code is very difficult to read. Testing things locally is a nightmare, since stack traces are a ridiculous stacks of .invoke, as if Java programmers are afraid of directly calling functions.
I let my frustration get the better of me and I apologize.
It's not just "for some reason". There are plenty of reasons why getters are a superior approach to naked fields, and these have been documented to death for the past twenty years.
If your field is not final, you should hide it behind a getter/setter.
Many other languages do just fine without them. What makes Java so different?
What makes Java different is that they have properties as a concept (in the Java beans specification) but not as an actual language feature, so you have to implement them manually yourself for every field and every class. (Or let the IDE generate the code, but all that boilerplate is code that wouldn't need to exist in other languages.)
1. Many other languages let you redefine field assignment syntax to be a function call, which is basically what a 'property' is in Kotlin. Java doesn't.
2. The Java ecosystem distributes software in binary bytecode form and cares about binary/API compatibility.
The combination of these two mean that even if you don't want to run any code on field assignment today, if there's any chance you might want it tomorrow you have to plan for it now by using functions instead of naked fields. Changing the latter to the former means updating all the use sites.
The big win of language integrated properties is you can go from (in Kotlin):
class Thing(val veryLongText: String)
to class Thing(text: String) {
val veryLongText by lazy { text.toLowerCase() }
}
without any of the places that use Thing.veryLongText noticing the difference. The first code just stores the string as an ordinary field. In the second, we've decided the text should actually be lower case and that lower-casing should be done on demand then memoized for efficiency.In Java, if you want to do that, you'd have needed to write a getVeryLongText() method from the start, as otherwise you'd have nowhere to add the extra code.
In a lot of cases, immutable objects are superior: simpler to reason about, have less code, and cost as much as mutable objects.
In Go - you can't do it like that. You need those getters, preferably an interface (because exported struct allows anybody to create a zero-value) and a factory function.
[1] - assuming their types are also immutable
Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
That said, the thing I like about Java is that its JIT compiler and bytecode system allow for efficient metaprogramming; Go's runtime package works, but it's not fast at all. These use cases are few and far between, but it would be nice if Go had a good web framework story (e.g. rails, django, spring, etc).
> Maybe conforming to an interface without explicitly declaring it (like you declare an instance of a typeclass in Haskell) may look like a controversial decision. I still think it does more good by removing boilerplate than bad by accepting an object as conforming to an interface in a rare case when you did not mean it.
Yeah, I hear this objection pretty frequently, but I've literally never heard of a single instance of this resulting in a real production bug. If it's a rare case, it's vanishingly rare, while it's no exaggeration to say that virtually every project benefits from the advantages.
The language may be lacking here and there, but the tooling around it is good, and the runtime of it is great (sub-ms garbage collection, etc).
Unfortunately, inheritance is deeply ingrained into many libraries and standard classes, though it has been replaced by interfaces in most standard APIs.
Yes, the boilerplate culture is a problem, not of the language but of the ecosystem :-\
You can stay reasonably away from it in many cases with modern libraries, though.
Could you please provide an example in both Go and Java? I am not quite sure that it has to do with the type system itself but your familiarity with the language and its libraries, and/or lack of basic functionality that should be in your text editor when you write Go.
value := getValue()
Java:
int value = getValue()
var value int = getValue()
Alternatively, use your text editor. For me, even Emacs displays the type when my cursor is on or around value, and if you put it on or around getValue(), you get its function prototype that includes its return type.Once you get into more complicated types things get even harder to understand.
var value = getValue()
So it's almost exactly the same in both languages. It's not a problem for most IDEs, just hover your mouse over the value or use keyboard shortcut to get the type. This was never an issue for me in Java or Go.
If you're writing a lot of boilerplate in Java, you're either doing it wrong, chose it for something wildly out of its wheelhouse, or you choose frameworks or libraries that have either forced or afforded high boiler-plate code.
The latter is probably the major difference that is what people are feeling. On paper Go and Java are virtually the same language, but just a few differences in the spec and a bit of focus by the developer community means that Go just generally has less annotations, external XML files for this, weird build systems, and a whole bunch of other frippery.
Although I do think that it's easy to underestimate how much cleaner it turns out to be to do interfaces the way Go does rather than Java. It seems like such a small change, but it has a big effect on how much OO boilerplate you need. You don't have to guess what interfaces someone may someday need and write them all out in advance.
And, I mean, in both languages, let's not underestimate the amount of "doing it wrong". There's a lot of us whose exposure to Java is being on the receiving end of a pile of Java code that is not written with maximal skill and precision. That's not really Java's fault. (To the extent it is, it is not uniquely so; I can tell that story about a lot of languages.)
Like shoehorning a set use case into a map? Or trying to get the list of keys or values in a map without writing out 5 lines of code?
I find golang code to be much more verbose than Java. You find code littered with one off functions that amount to nothing more than map/filter/etc. operations which are 1 liners in Java.
(I am underwhelmed by the crowd that insists map/reduce/filter is so important anyhow. I've used a substantial amount of Haskell. In Haskell, map/reduce/filter is the beginning of the functional style, not the completion of it. It goes beyond just "an alternate way to write for loops", which is most of what I see non-Haskell usage doing with it. In Go... just write the for loops. It's the same thing, just spelled differently. In Haskell, it's not the same thing.)
How else do you suggest transforming slices from one type to another, removing entries in slices based on certain conditions, etc. without looping? This is one of the most common operations in any basic application.
I'd have preferred much lambdas just exposing checked exceptions they experience inside. I can't imagine why the compiler shouldn't be able to do this. Maybe one would have to come up with a fancy definition of functional interfaces (although I think this could be tackled with an annotation if the language doesn't provide this for lack of another keyword a la "throwsall").
If you really want something like checked exceptions then (anonymous) intersection types or row polymorphism would be much better approaches than the adhoc thing Java does... but I suspect that that wouldn't well with the variance system in Java.
(Thankfully it's only the Java compiler itself which imposes checked-ness, so other languages on the JVM thankfully don't have to deal with the insanity.)
The lack of sum types and pattern matching notwithstanding, such a signature can be implemented reasonably in Java.
One, why do you think almost all non-standard java libraries almost exclusively use exceptions derived from RuntimeException?
Two, I encourage you to look up guava's Throwables utility class and consider why it is even a thing. Just for a single example: Methods with no throws clause cannot throw exceptions, so you must wrap in a RuntimeException... but that means that outer catch clauses which match on the checked exception WILL NOT MATCH on the RTExc-wrapped-exception. This is broken as all hell.
It's simply broken.
EDIT: Just to add: What happens in practice is that either
- you just give up and declare "throws Exception" on the interface (in which case: why bother? You might as well just remove checked exceptions at that point), or
- you wrap checked exceptions in RuntimeException, but now you have a new problem: Consider a method that calls methods f and g where f declares a "throws Foo", but g doesn't declare any exceptions[0], but may throw Foo's at runtime by wrapping. Now you're totally screwed, because a "catch Foo" will not match exceptions from g. The Guava people have written a whole Throwables utility class to help with this, but using that consistently requires discipline that no team can handle, and it only handles a small subset of the problem.
This is just a small sample of the problems with checked exceptions in Java.
[0] g may be used in contexts where it's not "allowed" to declare checked exceptions in a throws clause, e.g. lambdas or just higher-order functions in general.
if e3 != nil {
return _, e3
}
over and over and over.. e3?
^ Go could very easily add this syntax, but haven't for some reason.If you restrict yourself fully to the standard library it's still not that largly exaggerated, even with Java 8.
> The compiler has a well-defined custom processing interface (@annotations) which allows for powerful boilerplate-reducing things, from Lombok to Spring to JUnit.
If you add Lombok + sth. like guava or apache-commons and in general choose modern libraries/frameworks it's really OK now. The only downside of Lombok is that it really maxes out what compiler(s) allow you to do which is not always without issues and it requires an IDE plugin to be installed.