Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Background: how we got the generics we have (2020) (java.net)
68 points by kasperni on April 1, 2021 | hide | past | favorite | 101 comments


One flaw of the article, that is indeed interesting, is that it seems to ask us to accept a fact that serves little purpose. Yeah, we understand why the designers made the choice because it was the best for them, and they did think a bit of their users, but just a bit.

Decades later, we're still wondering as we get to more and more complex use cases, why we're stuck with passing the class object to all our generic types just to know who we are. It's really a rare instance where Java gave an engineer's solution when they should have given us something more, like a compile flag, a fork, a GenericArrayList<Something> that would be reifed, anything, rather than excuses about ecosystem, backward compatibility perfection, etc.

I understand why they took that decision, I don't understand how it helps all of us do meaningful generics :(


Without this requirement, generifying a class would require a “flag day” where all clients and subclasses have to be at least recompiled, if not modified – all at once. For a core class such as ArrayList, this essentially requires all the Java code in the world to be recompiled at once (or be permanently relegated to remain on Java 1.4.) Since such a “flag day” across the entire Java ecosystem was out of the question, we needed a generic type system that allowed core platform classes (as well as popular third-party libraries) to be generified without requiring clients be aware of their generification. (Worse, it wouldn’t have been one flag day, but many, since it is not the case that all the world’s code would have been generified in a single atomic transaction.)

Another way to state this requirement is: it was not considered acceptable to orphan all the code out there that could have been generified, or make developers choose between generics and retaining the investment they’ve already made in existing code. By making generification a compatible operation, the investment in that code could be retained, rather than invalidated.

It's great to see such care for the users, while at the same time improving the platform to make it one of the top performing ones.


Fun fact - if you don’t like Java erasing types by default, you can just pass them along manually yourself.

There’s a bunch of ways to do this, but I’ll just leave this one trick here.

Instead of:

    List<Integer> x = new ArrayList<Integer>();
write this:

    List<Integer> x = new ArrayList<Integer>(){};
Now you’ll be able to ask at runtime and determine it is a subclass of List<Integer>.


This is a great write-up. Accessible but thorough.


> This would have devalued almost all the Java code in existence at the time.

So, instead of doing that, they have decided to devalue Java code in long term, by deliberately making language and runtime less useful. Interesting choice.


I think the way Rust took with Generics should be the next step for Java, it for sure can boost the Java apps performance. In Rust you have both types erasure and C++ style templates which don't erasure the types.


I don’t think templates are wanted, but with the coming of unification of primitive and reference types, Java could get a huge speedup in performance.


This has passed me by, what's happening?


Not so much a unification, as the new constructs sort of enhance and retrofit current primitives types. These new primitives are "class instances that lack object identity and can be stored and passed directly, without object headers or indirections".

It will allow (almost) direct control over data layout. At least make it so that it is cache/prefetch/SIMD friendly.

Here is the JEP: https://openjdk.java.net/jeps/401


> the way Rust took with Generics should be the next step for Java

I have a List<Integer>. Maybe I want to replace List with something else, call it L.

Can I write L<Integer> in Java? No. Rust? No. C#? No Kotlin? No.

Does Go have generics yet? Once it does, I'm sure it'll be a No as well.

Haskell and Scala got it right.



I don't understand what you're saying, what is the problem with L<Integer> ?


The feature that mrkeen is describing is called higher kinded types (HKT). I.e. in Scala (and similarly in Haskell), you could write something like:

    def process[L[_]](input: L[Int]): Unit = ???
Where `L[_]` could be any type that has a type parameter, e.g. `List[_]`, `Option[_]`, `Future[_]` or `Either[String, *]`. This opens the door to a new level of abstraction where other languages must resort to code generation or similar ad hoc-like solutions.


oh, I see thanks for the explanation.


There shouldn't be a problem with it!

    <L> void process(final L<Integer> input) {
    }

    ------------------------------------------

    <L> void process(final L<Integer> input) {
                           ^
    required: class
    found:    type parameter L
    where L is a type-variable:
      L extends Object declared in method <L>process(<any>)


Works perfectly:

public <T extends Collection<Integer>> T process(T collectionOfInteger) { return collectionOfInteger; }


This uses subtyping polymorphism not higher-kinded parametric polymorphism. If subtyping worked perfectly in all important cases, "generics" (parametric polymorphism) would not have been retrofitted to Java.


What if your "T" isn't a collection though?


Yeah, as Joker_vD said you need _some_ common ground as to where you get your methods from.

I dream about "implicit" interfaces in Java like in Go. So everything that has "flatMap" would implicitly implement "FlatMappable" or something like that.


Then what can you really do with it except of returning it back as-is?


Nothing - unless you also take a function that allows you to transform from one type to another. For example, you can write a function like:

  S<U> map(S<B> b, (B -> U) func) { //stuff}
Which allows you to do things like:

  Optional<Integer> i = 4;
  (Integer -> Float) func = (i \* 2.5);
  Optional<Float> f = map(i, func);
This ability becomes much more powerful when you're parsing some file. Imagine you're parsing some JSON of people, and you're using https://schema.org/Person as the type. So the JSON contains things like additionalName, affiliation, birthPlace, email, gender, image, etc. and each of those are typed respectively - a Name class, Affiliation Class, Location class, Email class, Gender class, Image class, etc. Now imagine that the service you're calling may fail to return certain values.

In java, it's idiomatic for libraries like Gson to return null on the fields that aren't returned - so if you try to get a person who doesn't have an additionalName, the additionalName field in the Person object will be null. It would be nice if it could be an Optional<AdditionalName> instead of just an AdditionalName to force the null check, but then the library becomes extremely painful to use. You would end up having to call additionalName.get() whenever you just want the name. If you have HKTs, then you can operate on the fields and ignore the fact that they're technically Optionals.

It's also generally recommended in Java to not use Optionals in fields in classes, or to express the optionality of a value in a constructor. If you do, you have to wrap values in Optionals to make the object, which harms readability.

HKTs can become even more powerful with types other than Optional/Maybe. Either, Monad, Future, Comonad, IO, Functor, Applicative, Task, etc. You would be surprised how many design patterns in Java end up being just a simple combination of these types that are much more ergonomic with HKTs.


Indeed, without a type-class constraint on the type constructor this isn't useful at all.

Something like the following Scala 3 code is closer to the actual intend, I think:

  @main def entryPoint =
  
    trait ConsoleLogger[W[_]]:
      extension[E] (w: W[E]) def log(): Unit

    given ConsoleLogger[List] with
      extension[A] (list: List[A]) def log() = println(list)

    given ConsoleLogger[Some] with
      extension[A] (some: Some[A]) def log() = println(some)

    def process[E, L[_] : ConsoleLogger](input: L[E]): Unit =
      input.log()

    process(List(23))
    process(Some("'process' is generic!"))
    //process(None) // Does not compile — even Some and None are interchangeable under subtype polymorphism
Here process(input) can work with any wrapped input as long as a ConsoleLogger instance is given for a specific wrapper type. The point is: There doesn't need to be any relation between the wrapper types (like in the example with List and Option.Some), only the "shape" needs to match W[_] (which could be actually also adapted with type lambdas in case it doesn't match, but not going into this here).


What operations can you perform on an L<Integer>? Does it work based on duck typing instead of explicitly declaring what base interface (Collection<Integer>) it requires?


Generally you are using a typeclass to specify behavior (which I guess is a kind of duck-typing).

For example, there is a typeclass Foldable[F[_]] which provides the ability to "fold" (i.e. recurse over) some type F[_] which takes a type parameter. Then you have typeclass definitions for Foldable[Set], Foldable[List], Foldable[Tree], Foldable[Option], etc.

Then if you have an operation and you want it to work on any collection that can be folded, then you can just define it in terms of a generic type that has a Foldable typeclass. For instance to generically sum a collection of ints, you might do

def sum[F[_] : Foldable](ints: F[Int]): Int = ...

Now you can call sum(List(1,2,3)), sum(Set(1,2,3)), sum(Option(1)), etc...


Typeclass instance = Adaptor/Decorator in OOP terms, automatically derived (and wrapped in) for you by the compiler.

You want to fold over Options? Make a FoldableOption class implementing Foldable<Option> and pass "new FoldableOption(myOptionValue)" to whatever places you need. Haskell does pretty much the same (unless they moved away from dictionary passing).


It's a similar idea to inheriting from a base collection class (iterable, etc). Not quite the same though. It is a kind of duck typing I suppose. You get to specify the nature of a class of types. e.g. it can be ordered, behaves like a number, etc.


No worries in Java (and I bet in C#) in works no problemo:

public <T extends Collection<Integer>> T process(T collectionOfInteger) { return collectionOfInteger; }

And basically your T will be as wide as the interface/class it extends in the generic parameter.

I guess the most Java way to do that would be using Iterable:

public <Z, T extends Iterable<Z>> T process(T iterable) { return iterable; }


I want to do something like this:

public <Z, T extends Iterable<Z>, R> T<R> process(T, Function<Z,R>)

change the type parameter of T<Z> to T<R> and return an instance of T<R>, but the best I can do is return an instance of Iterable<R> because I cannot define T to have a type parameter.


Sooooo.... A bit more involved than what you describe but I managed to do that:

    public <A, B, T extends Iterable<A>, R extends Iterable<B>> R process(T input, Function<? super A, ? extends B> transformer,
                                                                          Function<Iterable<B>, R> helper) {
        return helper.apply(StreamSupport.stream(input.spliterator(), false)
                .map(transformer)
                .collect(toList()));
    }

    @Test
    public void testProcess() {
        Iterable<Integer> ints = process(Arrays.asList("1", "2", "3"), Integer::parseInt, identity());
        System.out.println(ints);
    }
Outputs nicely integers nicely converted from strings: [1, 2, 3]

:-P

`identity()` is a static import of `Function.identity()`


That's close, but not quite what I want. The Iterator example starts to break down, but I want to pass in a subclass of Iteratable and get that subclass back.

class SpecialIterator<R> extends Iterator<R>{}

SpecialIterator<String> strings =...;

SpecialIterator<Integer> ints = process(string, magicFunc);

The Iterator example starts to break down, so here's a more concrete example:

I have 3 classes: AbstractTester, StringTester, and ListTester

class AbstractTester<R, T extends AbstractTester<R, ? super T>>

class StringTester<R> extends AbstractTester<R, StringTester<R>>

class ListTester<R, T extends AbstractTester<R, ? super T>>

I want to make a method in AbstractListTest like this: public <Z> T<Z> convert(Z z)

I have supporting code in my classes to create an return an instance of type T<Z>, but I cannot express the type T<Z> in the type system. The type parameter T can extend something with a type parameter, and be passed a type with a type parameter, but it cannot be defined to have a type parameter itself.

This is for a fluent api framework, where I need the compiler to know the type T<Z> without casting

I can do something like this: public <Z, Y extends AbstractTester<Z, ? super T>> Y convert(Z z);

but that will return an AbstractTester<Z> which can be cast to a StringTester<Z>, but the compiler does not know it is a StringTester<Z>

A subclass of AbstractTester is not required to have any type parameters itself. Converting a T to a T<Z> would not be legal for a T without a type parameter. To do what I want, Java would need to add a new type bounds operator to restrict a type parameter to subclasses with a particular type parameter.

Java would need to allow me to define ListTester like this to restrict T to only subclasses with a type parameter:

class ListTester<R, T<_> extends AbstractTester<_, ? super T<_>>

I'd love to be wrong, and would be very grateful to find a way to do this.


The only caveat is that if you change `Iterable<Integer>` to say `Iterable<String>` the compilation error becomes a bit weird in that it says you provide `Iterable<Object>` while `Iterable<String>` is requested. But you do get compile time check after all.


T is not R though. I want to preserve the static type information such that if I call:

    baz(bar(foo(t, f)));
T<A> should go into foo(..) and come out (e.g. as T<B>) such that I can then call bar(..) on it.

But in this case it leaves foo(..) as R extends Fooable, which is not something I can pass into bar(..).


I’d just solve this by passing in a Class<S extends Iterable<R>> arg and making the returned list be an instance of this class, instead of forcing the input collection type to be the same as the output collection type.


Then I can't chain method calls.

    foo.doSomething()      // returns Iterable.
       .doSomethingElse(); // Iterable doesn't implement doSomethingElse();


This comment explains my problem much better than I did.


See my other comment for a longer explanation.

I'm curious what your were working on where you ran into this?


* At a previous job, I needed to change a fairly large codebase from using Guava futures to Java 8 futures.

* At the same job, a coworker expressed a desire to rewrite it with Observables.

* At a different job, a different coworker rewrote a chunk of code from Observables into futures.

* At my current job, we use micronaut, which has announced it's switching from rxjava2 to project reactor.

In the above cases, having interfaces would really help in changing the code piece-by-piece. And the rewrites wouldn't be necessary if they were behind interfaces.

In the general case I just want to program to the interface, and not pin my business logic to any particular implementation.


see my comment for the parent with _a_ solution =)


I can turn any collection to list like that:

public <Z, T extends Collection<Z>> List<Z> process(T collectionOfInteger) { return collectionOfInteger.stream() .collect(toList()); }


Great!

These are the three implementations I want to hide behind a single abstraction:

    // java.util.Optional
    public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)

    // java.util.concurrent.CompletionStage:
    public <U> CompletionStage<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);

    // java.util.stream.Stream:
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
Any suggestions?


I'm not sure if I catch your drift, but if I do:

    public <T, Z, F extends Function<? super T, Z>> Z apply(Function<F, Z> target, F fn) {
        return target.apply(fn);
    }

    @Test
    public void test() throws Exception {
        Stream<String> stream = Stream.of("foo", "bar", "baz");
        stream = apply(stream::flatMap, s -> Stream.of(s + "_a", s + "_b"));
        System.out.println(stream.collect(toList()));

        Optional<String> bean = Optional.of("bean");
        bean = apply(bean::flatMap, b -> Optional.of("soy"));
        System.out.println(bean);

        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "hello");
        CompletableFuture<String> future2 = apply(future1::thenCompose, string -> CompletableFuture.supplyAsync(() -> string + " world"));
        System.out.println(future2.get());
    }
Output:

[foo_a, foo_b, bar_a, bar_b, baz_a, baz_b]

Optional[soy]

hello world


This is interesting stuff.

I'm trying to figure out how to refer to flatMap directly from the abstract function, e.g.

    flatMapTwice(x, fun) {
        return x.flatMap(fun).flatMap(fun);
    }
or

    chain(x, fun1, fun2) {
        return x.flatMap(fun1).flatMap(fun2);
    }
It should also preserve the type information (just like you did above). I don't want the caller to need to downcast from FlatMappable.

It doesn't need to be x.flatMap(f) - I'd be interested seeing a flatMap(x, f) version.


Sure:

    public interface FlatMappable<T> {
        <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper);
    }

    public class FlatMappableOption<T> implements FlatMappable<T> {
        private final Option<T> value;

        public FlatMappableOption<T>(Option<T> value) {
            this.value = value;
        }

        <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper) {
            return value.flatMap(mapper);
        }
    }

    // similar wrappers for CompletionStage and Stream
Now whenever you want to pass a Stream/CompletionStage/Option to something that accepts FlatMappable, you either a) already has it wrapped in FlatMappable, so pass that wrapper in; or b) has a naked value of known type, so wrap it in the propper wrapper class and pass the resulting wrapper in.

Of course, it would be nice to have this wrapping done automatically instead of manually, but that's pretty much how it's actually implemented behind the scenes (dictionary passing, or look at Go's way to wrap things inside interface{}).


Also I don't understand why would you need HKT here. I don't see language limitation here, I see poorly designed inheritance in the standard library. They should've introduced "FlatMappable" type and make Option and Stream extend it. CompletionStage, I'm torn on it, on one hand yes, it is a flatMap essentially but semantics is a bit different...


Oh my god, there's more to that, Optional is a class, not an interface and it's final :facepalm:


I've made my peace with those aspects of the libraries. I'm happy to wrap the standard classes in MyOption, MyStream, etc. Then I can extend FlatMappable myself. I just don't know how to declare and implement FlatMappable in a sane way (preserving type information, not needing casts, etc.)


A Parametric VM [1] is part of Java's project Valhalla. However, it is currently some releases away.

[1] https://cr.openjdk.java.net/~jrose/values/parametric-vm.pdf


Very cool, it seems to be the best of both worlds, the specialization of the class/fields is done the first time a specialized type is instantiated while the specialization of methods is done by the JIT.

And the VM can specialize on any values, not just types.


There is no next step for Java, because of backwards compatibility this is as good as it gets.


While it does need to maintain backwards compatibility, Project Valhalla seems promising on improving on some of Java Generic's pain points. So it's not a dead end quite yet


alternate url also submitted 9 months ago, I dunno

https://news.ycombinator.com/item?id=23621185


Kind of off topic, but is something using either Lombok[0] or Manifold[1] to have better ergonomics in Java? It sounds like a brilliant idea to me, but I don't use Java on a daily basis.

[0] https://projectlombok.org/ [1] https://github.com/manifold-systems/manifold


Lombok is very widely used in places I worked. Love it myself.


The problem I have with Lombok is that suddenly it’s hard to trace getters and setter from the Pojo. By this I mean asking the question, “where is getId() used” is hard if I want to use the find references feature of the IDE.


I also have mixed feelings about Lombok.

For example, if you want to automatically create getters and setters to all private variables, why not make them public in the first place?

I hate when tools mess with IDE. What about guys using Emacs (or any other editor that does not understand Lombok)? I think generating code from IDE is fine as well as having code generated from build system distributed along with code, but having that code generated through magic of annotation processor is not as nice. I also suspect that Lombok messes my IDE on a deeper level but I have not yet a good concrete proof. Recently I am more and more frequently finding situations where Idea messes up understanding correct types of things and it takes for it some time to catch up (and sometimes reloading the file).

On the other hand I love small things like creating builder or logger. Which is to say these can be just as easily generated from a template, no annotation processor needs to be involved.

I think main feature of Java for many years were its IDEs which would allow to run accurate refactorings or always understand who is using particular piece of code, what is exact call hierarchy, etc. I feel loosing that is going to make Java much less useful for me when I try to analyze large project or run huge refactorings.


>why not make them public in the first place?

If you're in an old school "OOP" style java codebase, where each class is its own little island of encapsulated state and behavior, the coupling of the two means that strictly controlling access via getters is usually required.

Even in more "modern" java, where you do most of your programing against plain data, having Lombok attach getters still gives you some quality of Life benefits because you can use method reference rather than anonymous functions for high-order access (e.g. `things.map(MyClass::getName)` rather than `things.map(myclass -> myclass.name)`)

I'm with you on mixed Lombok feelings, though. It's great right up until its not. It fails or doesn't work in really unexpected or weird ways, and is the source endless pain when you've got other annotation based things (like dagger) which then imposes annotation processing order failures.


> If you're in an old school "OOP" style java codebase, where each class is its own little island of encapsulated state and behavior, the coupling of the two means that strictly controlling access via getters is usually required.

That I agree. My comment was meant to be cheeky a little bit as it is actually pretty difficult to find trully OOP application, at least in area of backend apps I am working in.

Obviously, in OOP you are not supposed to expose your internal state but rather accept messages to run behaviour.

If you are working on a truly OOP application then Lombok is useless. Your public class interface is all that matters then and you would spend more time fighting Lombok than just writing it manually.

Lombok is only good at automating boilerplate which, if you have a lot of, is a sign of some other problem.

> It's great right up until its not. It fails or doesn't work in really unexpected or weird ways

That is exactly the point. Magic is fun until you find out that it leaks horribly and causes unintended side effects with all sorts of stuff.

What's the point of replacing simple problem (just really use templating mechanism you have in your IDE) with complex problem (dealing with magic failing on you).


> why not make them public in the first place?

Because in future I may want to add some validation there, or processing, like sending events on set of a value, etc.

Properties in C# was a genius move that solves it correctly.


Unless we are talking about API, then you add getter and setter. You might even be able to refactor all callers automatically via IDE. I mean, I dont mind getters and setters in java, it is standard at this point.

But origin of it is in EJB specification, it is not something that would be interently needed elsewhere. It just "feels wrong" to programmers (including me) at this point.


That, in my observation, almost never happens.


I'm working in a product that is about 10 years old and 4 months ago it went through a quite a big project which purpose was to add extended auditing to be more enterprise friendly. The system was flooded with auditing events on various changes into internal data. The system provides a public API and allows to install plugins. If we had to change our entities that would be a compatibility nightmare for thousands of plugin vendors.


I'm not sure how this works in other IDEs, but lombock Eclipse plugin kinda solves this problem. You can run CTRL+Shift+G (Find references) on getter/setter that is shown in class outline and it will immediately give You getter/setter uses.

https://stackoverflow.com/questions/42644923/eclipse-with-lo...


Depends on your IDE. IntelliJ IDEA supports this with the Lombok plugin


At least with Lombok, I've found it makes builds and tooling flaky, so I'd rather write an extra line here and there (or use Kotlin).


We use it, but I don't like it. I does superficial stuff for me, but also makes navigating and reading code harder. It takes somewhat more time to figure out all setter callers and stuff like that.

It is one of those things that does not much useful, but takes away only little bit so there is no reason to fight it too much.


Never heard of manifold. Thanks for sharing.


I really don't enjoy anything about Java except for the JVM and if I press somebody that advocates for Java about what the reasons are that I should use Java it's always about either the JVM or the fact that there's so much of it around. But never really a solid argument why the language itself is nice to work with.

If I would want to use something like Java for a project, I would always pick C# which is a better Java in all aspects.


The relative simplicity of the language allows it to have excellent IDE support. I can select a class name, rename it, and have it renamed in all files that use this class. And the IDE knows that this actual class is being used, not another class from another module that has the same name, and that it's not a string with the same value as the class name. With languages like C++ all you can do is grep and cry.


OP mentioned C#. Exactly the same is possible there.


Yes and no. The C# language does have conditional compilation, which is modelled after the C preprocessor. That can interfere with refactoring and limit its accuracy. The good thing is that this mechanism is rarely used in practice.


Does the LINQ expression tree stuff complicate things? I don't know how that works with respect to parsing.


Visual studio has c++ symbol rename, including class names. Has had it for a while as well. Fairly reliable, though occasionally it does fail, loudly.


> a solid argument why the language itself is nice to work with

Java with IntelliJ is awesomely productive. You barely have to type anything beyond new variable / field names, everything else is generated or tab completed. Refactoring is a breeze.

That, times 'worse is better' - it's the language and toolset I already know for the types of things it's good at. I have no doubt C# is comparable or better at most things, but probably not so much so that it is worth switching.


> Java with IntelliJ is awesomely productive.

They have a C# IDE (Rider) now, based on IntelliJ. While it was originally the only non-Windows C# IDE, I and others have switched to it from Visual Studio (which is almost as good) even on Windows.

> I have no doubt C# is comparable or better at most things, but probably not so much so that it is worth switching.

I’d say you are right. Now. 10 years or so ago (6/7/8 times) I’d have disagreed, C# avoided many of the mistakes* Java made and was able to become far more pleasurable to use, it took some time for Java to catch up again.

*: Mistakes only in hindsight


Why does it always has to be language 1 vs language 2? "Better" is hardly something objective about languages. This is why people are still building new languages every year. To make things "better", although ultimately a bunch of others will find quite a big list of why they don't like it. Just pick whatever language you like. As long as you have good tooling and community around it, it won't be neither the best nor the worst choice... It's just "a choice"... Projects hardly fail because of the language itself (although there are some well known failures with dynamically typed languages). It's mostly bad architectural or business decisions...


> "Better" is hardly something objective about languages.

Rust is better than C

C# is better than Java

You can talk about tooling, environments, how many people use it, what you can use it for, but when it really boils down to the language there are languages that are better than others while trying to achieve the same.

I mean I use Elixir for other things than Swift or Rust or F#, Elixir is the best language for highly concurrent "message passing" systems. Some features of Elixir that I don't immediately like like it's type system unlock possibilities that no other languages have. But I don't see anything like that in Java, unless I really look around in 1993 and the only competitors are C++ and nobody-uses-it-yet Python.


I think that in comparison, what happened with Javascript has been great honestly. People have been completely stuck with it, so we've built a great server side (Node.JS / npm / yarn) and client side (React, Vue, Typescript, Babel) ecosystem around it. I think great things can happen if you stick with a language and work with that limitation.


Things will be really exciting once WebAssembly finally breaks through (Next year in Jerusalem!...) and we're not forced to compile better programming languages to Javascript.


From what I have seen, I'd probably pick Kotlin if I had to be on the JVM. Seems to strike a nice balance between sanding off the rough edges of Java syntax, and still being conventional and blub enough that it won't put people off.


Are you assigning any cost to : Kotlin is not Java and therefore new tools, developer training, etc are required? I'd want Kotlin to be significantly better than Java to justify the fact that it's not Java, if you see what I mean.


You can write Java in Kotlin in less than a day.


Not sure everyone agrees but each time I've used type inference language (really just Groovy to be honest, but many many many different teams and companies), it ended up with an enormous mess of code that the brain of all of us devs had to parse themselves. We rolled back to Java many such critical paths just to be able to maintain them.

My little pitch when starting a migration project in a distressed no-type language team is to make them read the code and explain it, while asking about the type of objects they use and show them when they read it's their brain parsing it with high effort. If they de-optimize and just write the types more often, it becomes much clearer longer.

You can type usually in those language but often people want to use the "proper way" as shown in ultra simplistic examples in manuals for sales pitches and end up thinking the right way to code is to write as little as possible (combined with the no-comment belief they have but don't map to "no comment because clear enough code").


I mean, Groovy is terrible, and there is not very good tooling for it.

That's not the case for Kotlin, that has the full weight of JetBrains behind it.


> We rolled back to Java many such critical paths just to be able to maintain them

I guess that's what I see as one of the advantages of Groovy. You can do ultra rapid implementation of something with minimal code and then as it becomes more critical you increase its type safety by adding first Groovy type hints, but further along that curve, convert it fully to Java or another statically typed language. So what you describe as a problem I see as sort of, optimal workflow, and the real problem is being forced to universally choose one or the other in other languages.


> But never really a solid argument why the language itself is nice to work with.

If you were trying to write portable C++ code in 1996 you would get it, specially when using each OS specific C++ compiler provided by the platform vendor.


We cross-compiled to get gcc on every platform. Even then, it was still:

   #ifdef _AIX
   ...
   #else ifdef SGI
   ...
   #else ifdef WINDOWS
   ...
   #else // everything else
   ...
   #endif
Shudder.

But the other thing that Java had is the library. Compared to C++'s standard library in 1996, it was a revelation.


True, a decent standard library is really important. Every language I enjoy to work with has a good way to do all of the standard stuff


Exactly, at the time it was invented, Java was a massive improvement on the incumbents in all sorts of ways


Your first paragraph is a fair encapsulation of what Java was intended to be from 1994: a mullet of a language that is deliberately unexciting at the language end, but brings in a number of (at the time exotic) features at the VM end.

Things like GC, reflection, runtime loading of code, that were very familiar from more academic languages at the time.


Well I may try ! I'm not very religious, I used Java for everything since I've started programming and I work in low latency in an investment bank now. I adore Java, it's my truly native programming language, I try to know all of its quirks, study each new version and know still not enough to satisfy my love of it.

I also did a lot of C# to help on a GUI optimization project (because doing low latency if the GUI freezes every 10 minutes isn't great), and I was really surprised by how good it was so I wouldn't disagree it works nice.

BUT I like that Java works simply in linux, I like that it didn't yet got contaminated by all the crazy constructs from elsewhere (Groovy grr), I love the tradition around the syntax (I can't stand C# quirks sometimes). I do HATE with passion the meaningless generics, and I wish we'd ditch backward compat to steal it all form C++ templates.

I enjoy more than the language syntax itself that is close to most languages I like (Javascript, C#, C++), the team spirit around it. Believe me, it's much easier to use and understand maven (or maybe gradle who starts to get dangerous in real world projects where you can't afford 2 weeks of reverse engineering a madman's build script) than it was nugget, it's easier to use IntelliJ than to receive yet another stupid email from compliance about a Visual Studio license missing, it's nicer to build a jar and run it than to go through all the kinks of exe files... I dunno I may have started from Java and got to C# and done the opposite of your journey, but while I agree C# has superior features in some aspects (but sometimes they chose mess vs safety) I just wasn't convinced it could work for something more critical where you must do things as simple as you can, as clear as you can.

And let's not even start on C++ who's superior in every conceivable way except that it encourage, or doesn't discourage, every team I ever meet to create unbearable messes they can't fix, so focused they are fixing problems we can't even imagine in Java for reasons we can't comprehend our company even accepts to finance :D


Does C# handle generics better than Java?


C# implements proper reified generics. Generic types parameterised with different types are actual types in C#, while in Java generics is basically just syntax sugar for avoiding type casts. Each element in a List<byte> in C# takes 8 bits, while in Java each element in a ArrayList<Byte> is actually just an Object and therefore is a pointer (64 bits) to a separate allocation.


But at the same time, reified types will create a separate object for each template type, making some optimizations happen only for a type that was used often. While in Java, ArrayList for example will benefit regardless of the concrete type.


As "better" isn't clearly defined, here are some things that some people might consider superior:

- primitives or any custom value types/structs can be used. No need for boxing or method duplication for primitives. - type information available at runtime, no need to pass 'Class<T>' parameters. - type constraint 'new' available, which means it must have a default constructor so you can do 'new T()'. No need to use reflection or pass a constructor parameter. - type constraint 'notnull' available - class-scoped compiler enforced covariance and contravariance, though I think you can get similar results with java's "? extends T" and "? super T" constraints


You can't easily create a instance of the generic type in java. for example :

return new T();

is possible in C#, not in java.


There is a way-ish, which is passing a Class<T> as an additional parameter and doing clazz.newInstance().

Definitely not pleasant though.


For that particular use case it may be better to pass a constructor reference / new instance supplier to not rely on reflection.


C# generics are reified.


Backward compatibility is one of the biggest reasons


Java has a much better AOP story, but that will likely change once source generators become more mature.


I find AOP dangerous. I love the abstract concept, but each time I used it myself I ended up creating a mess that I had to painfully rollback just to produce less insanity for my poor colleagues.

I'm sure it's me but do you use it yourself and why ? Don't you get lost in the indirections in big projects "oh shit where is that thing again that auto inject itself here" ?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: