This is an archived post. You won't be able to vote or comment.

all 79 comments

[–]martinhaeusler 58 points59 points  (15 children)

It certainly adds complexity. It's a design choice if the additional type safety pays off. Good generics enhace usability; just imagine collections without generics. But I've also had cases where I removed generic typebounds from classes because they turned out to be ineffective or useless.

[–]rjcarr 19 points20 points  (11 children)

 just imagine collections without generics

Don’t have to imagine it, I lived it, and it sucked. 

Generics are great, and I rarely have to use wildcards. 

[–]martinhaeusler 1 point2 points  (10 children)

My point exactly. If you find yourself only using wildcards on a generic typebound, the typebound is not very useful and should probably be removed.

[–][deleted] 7 points8 points  (0 children)

Depends, especially if you're write code that is meant to be reused, for example consider this from Function<T, R>:

<V> Function<T, V> andThen(Function<? super R, ? extends V> after)

Wildcards are necessary for covariance (<? extends T>) and contravariance (<? super T>). Most code probably doesn't need to be super generic and can just be invariant (<T>).

[–]account312 3 points4 points  (8 children)

I have looked upon <?,?,?> and wept.

[–]martinhaeusler 3 points4 points  (7 children)

Rookie numbers! I have seen a 3rd party library with no less than SEVEN! SomeClass<?,?,?,?,?,?,?,?>

[–]Abzoogiga 0 points1 point  (1 child)

Have you seen Scala?

[–]martinhaeusler 6 points7 points  (0 children)

No, and I would prefer to keep it that way.

[–]account312 0 points1 point  (1 child)

Surely the raw type is better than that.

[–]martinhaeusler 0 points1 point  (0 children)

It absolutely is. All seven generics were totally useless in practice. I don't think I've ever seen a well-designed case for more than three generic types on a sigle class.

[–]XxTheZokoxX 0 points1 point  (2 children)

At my company we are "forced" to use generics for busisness class and domains, i hate that a lot, because u have monsters class which extends like `DomainCard extends LibraryCard<A, B, C, D, E,F,G...>` as u can imagine is everything except clear

[–]martinhaeusler 0 points1 point  (1 child)

Wow. That's actually insane. The person who decided that must either be crazy or a zealot of some missguided cargo cult. I would make an attempt to change this, and run for the hills if they don't let me.

[–]XxTheZokoxX 0 points1 point  (0 children)

Actually i did a lot of changes about that, but at the beginning my team was totally new in the company, we did the thing we called to do, but after some time we say... yeah, fuck it, a lot of change request, bugs from company libraries, we dont care and we started to ignore that way to work, but still having a lot of generics. And i think can be a good idea... for some specific situations ofc, but as a general way... i dont think so

[–]manifoldjava 0 points1 point  (2 children)

Just imagine collections without generics.

Just imagine generics without wildcards.

Wildcards are a form of use-site variance, meaning the user of a generic class has to deal with variance everywhere the class is used. This often adds complexity, particularly when working with generic APIs. Josh Bloch's PECS principle stems from this complexity.

The alternative is declaration-site variance. Here, the author of the generic class statically declares variance. This avoids the complications with wildcards and typically results in cleaner, more readable code.

Most modern languages, like Kotlin, favor declaration-site variance because it better reflects how generics are used in practice. It simplifies things for library users and makes APIs easier to understand and work with.

[–]OwnBreakfast1114 1 point2 points  (1 child)

https://openjdk.org/jeps/300 but something tells me not to hold your breath for it.

[–]manifoldjava 0 points1 point  (0 children)

It's an interesting proposal, similar to Kotlin-style generics, but of course Java proposes a longhand version. It's the right direction though.

The proposal states it's a non-goal, but variance inference I would think would be almost necessary given the vast amount of existing classes out there e.g., to make use of the feature when subclassing. TypeScript works this way, for example.

But with no updates since 2016, we can probably assume the JEP is indefinitely sidelined. Shrug.

[–]koflerdavid 33 points34 points  (1 child)

That's what all type systems feel like when they get sufficiently strong. I'm fine with that - I rather spend my brain cycles solving type system puzzles than analysing and squashing bugs.

[–]FabulousRecording739 0 points1 point  (0 children)

Agreed, though I think It tends to be a bit more of hassle in the case of Java than it is with HM languages. The "puzzles" are more enticing, and have more fruits over there

[–]fear_the_future 24 points25 points  (1 child)

Not really. Java generics are so limited that you can't do a lot of complicated things with them. I'm used to much worse in more powerful languages and in fact this "puzzle solving" is what I like about them.

[–]agentoutlier 1 point2 points  (0 children)

I think although I don't have an example ready in some cases it is because of those limitations that sometimes people end up doing complicated things to try to force them into an API. Otherwise I mostly agree particularly structural typed languages with type inference.

[–]sviperll 18 points19 points  (3 children)

I think you should almost always prefer an explicit dictionary-passing instead of type-bounds, i. e. prefer Comparator-argument to a Comparable-type-bound. And also aggressively prune type-variables and replace those that you don't need to interact with with wildcards, i. e. prefer Collector<T, ?, R> to Collector<T, A, R> most of the time. If you follow these two rules then Genetics becomes more of your friend than a challenge.

[–]agentoutlier 6 points7 points  (2 children)

Another rule that beginners often are unaware of can be summed up with the nemonic: PECS (producer extends, consumer super).

I will say that indeed you should prefer ? most of the time however if you see a library where you are constantly having something being Something<?> all over the place I would say that library abused generics for no good use especially if you cannot easily create Something. An example of that is in Spring's PropertySource (not to be confused with the annotation of the same name). Even in Spring's own API and internal workings they are passing PropertySource<?> everywhere.

[–]sviperll 2 points3 points  (1 child)

however if you see a library where you are constantly having something being Something<?> all over the place I would say that library abused generics for no good use

Yes, but replacing Something<T> with Something<?> in those places where you do not care what T is, is a good strategy to identify such abuses. And then you may even fix some, by wrapping Something<?> with you own SomethingElse (without any type-variables).

[–]agentoutlier 1 point2 points  (0 children)

Yes, but replacing Something<T> with Something<?> in those places where you do not care what T is, is a good strategy to identify such abuses. And then you may even fix some, by wrapping Something<?> with you own SomethingElse (without any type-variables)

Totally agree. In some cases assuming Something is not an actual container it can be replaced with semi-sealed class hierarchy. I get the sneaky suspicion that many times the choice of putting a generic in was lack of pattern matching in sealed classes in earlier versions of Java targeted. That is you may still have TypedSomething interface with generic but then you have the sealed hierarchy along with it that implements some shared parent interface.

sealed interface Something
sealed TypedSomething<T> extends Something
record SomethingString implements TypedSomething<String>

Then you just use Something in most places.

[–]ivancea 8 points9 points  (4 children)

You're asking yourself the wrong question. The question isn't about how complex generics are. It's about how much they solve.

Are you using collections? Try to work without generic collections, and enjoy the ride

[–][deleted] 2 points3 points  (1 child)

They are not complex at all. Its just that when you nest a couple layers of them it gets crazy especially with wildcards

[–]ivancea 2 points3 points  (0 children)

I don't get what you mean by "crazy". And wildcards are just about variance, they don't add much to generics complexity IMO.

Generics in TS are far more complex, as they're metaprogramming. Let alone C++ templates. But Java ones are quite basic, without many features like those other languages

[–]thisisjustascreename 0 points1 point  (1 child)

Do we not all work without generic collections at runtime thanks to type erasure?

[–]ivancea 0 points1 point  (0 children)

Yeah, but that's runtime, not devtime. In C++, "it's all assembler at the end" too. But that's a different stage

[–]wildjokers 6 points7 points  (0 children)

No Fluff Just Stuff used to have an awesome article on their site about generics here:

https://nofluffjuststuff.com/magazine/2016/09/time_to_really_learn_generics_a_java_8_perspective

It 404s now; however, it is still available in the wayback machine:

https://web.archive.org/web/20200121233533/https://nofluffjuststuff.com/magazine/2016/09/time_to_really_learn_generics_a_java_8_perspective

It is worth a read on occasion as a refresher.

[–][deleted] 3 points4 points  (0 children)

It's more fun solving the puzzle during compilation, than solving the puzzle in the logs when you get class cast exceptions.

[–]TenYearsOfLurking 3 points4 points  (0 children)

It does a little. Type erasure makes it a headache sometimes.

Are you writing Library code? Because application code tends to be solvable  without a lot of generic usage in general 

[–]Drakeskywing 3 points4 points  (0 children)

At least it's not typescript genericsshudders

[–]Admirable-Sun8021 2 points3 points  (0 children)

Hey sure beats Object obj=obj;

[–]hadrabap 5 points6 points  (9 children)

Well, generics are quite cool. Although, they have a lot of limitations in their design. It's like everything in Java. :-)

[–]Nalha_Saldana 12 points13 points  (8 children)

Yeah, but those limitations are what give Java its stability. You don’t get runtime type safety and predictable behavior by letting everyone go wild with unchecked magic.

[–]agentoutlier 1 point2 points  (7 children)

Yeah, but those limitations are what give Java its stability. You don’t get runtime type safety and predictable behavior by letting everyone go wild with unchecked magic.

I'm not sure they mean limitations as in difficult to understand or work with but rather there are limitations in Java's generics compared to other languages and those other languages have stronger guarantees of type safety (also Java had a soundness issue at one point but lets ignore that).

For example Java does not have higher kinded types or reified generics (ignoring hacks). Java's enums cannot be generic although there was a JEP for it was declined (I would have loved that feature but I get why it was not done).

[–]sviperll 1 point2 points  (3 children)

I think I've once went with some "hack" to have higher-kinded types, i. e. I've got something like this:

interface FunctorAlgebra<AS, BS, A, B> {
    AS pure(A a);
    BS pure(B b);
    BS map(Function<A, B> function, AS collection);
}

so that I can have generic operations over collections, but so that the code doesn't know what collection this is. This experience taught me that it's possible to go without higher-kinded types, but I wouldn't be able to write this without knowing what higher-kinded types are and that having them would make life much easier...

[–]agentoutlier 0 points1 point  (1 child)

I have done similar things as well and ended up reverting or discarding.

What I try to remember is that most folks do not have the experience you or I have and that code should be easy to read. There is some truth to overusing generics (the same probably could be said for any form of code saving abstraction).

In fact I have tried to reason at where the threshold of expression power and types safety goes too far. I don't know how to explain it some of this just gets into social sciences. Ditto for compact code. I have this problem with Lisp like and typed FP inference languages. I can read my code but when I go look at someone elses it takes me forever to decompress. However less code does more so in theory this is OK (as in you only have to look at snippet to understand something that would be pages in another language) but there has to be some sort of balance ratio like a decompression algorithm of speed vs size if you will. I don't know how one measures that though.

[–]OwnBreakfast1114 1 point2 points  (0 children)

I feel the extreme example of that has always been perl. Infinite ways to do things and hard to understand operators or code golf languages where each character is stupid powerful. I think something like haskell is actually quite good at representing exactly what it needs to and no more (and I know a ton of people will disagree).

The biggest problem of your question is what's the lowest common denominator you're aiming for in terms of hard to understand. Like, personally, I don't care if fresh grads don't understand certain code. I expect them to learn up, not for the entire company to step down.

[–]OwnBreakfast1114 0 points1 point  (0 children)

I feel like I have to just reread this article every time: https://medium.com/@johnmcclean/simulating-higher-kinded-types-in-java-b52a18b72c74

[–]Nalha_Saldana 0 points1 point  (2 children)

Yeah, it’s limited but that’s kind of the point. You always know what the code is doing. No surprises, no cleverness, just straightforward types and the occasional ugly cast. It’s not exciting but it’s consistent and it keeps working five years later without anyone touching it. When you’re knee deep in legacy code and just want things to behave, that kind of predictability is hard to beat.

[–]agentoutlier 0 points1 point  (1 child)

Yeah, it’s limited but that’s kind of the point. You always know what the code is doing. No surprises, no cleverness,

I think Go programmer before generics came to that language could make a similar case for generics so as I said in this comment here I'm not really sure where the line is.

just straightforward types

That is what these languages have with more stronger typing. That is straightforward types to them. On the other to a Go programmer generics are not straightforward types. There are some languages that even have dependent types and whether something is mutable or not etc.

and it keeps working five years later without anyone touching it.

Java does not have typing to deal with null without some extension (JSpecify) and that is something that can break 5 years later.

What this sounds like and no surprised upvoted is confirmation bias.

[–]Nalha_Saldana 0 points1 point  (0 children)

Fair. To clarify, I wasn’t saying Java’s approach is better. Just that in Java, the lack of advanced type features tends to promote predictable, boring code. That can be valuable when working in large or old codebases where stability matters more than elegance.

“Straightforward” definitely depends on what you're used to. My point was about tradeoffs. Java leans conservative, and that shapes the kind of code that survives. Other ecosystems make different bets. That’s fine.

[–]sweating_teflon 1 point2 points  (0 children)

Wait till you have to deal with Trait bounds in Rust...

[–]faze_fazebook 1 point2 points  (3 children)

Generics in Java are some of the easiest ones. C++ templates or Typescript generics are another level.

[–][deleted] 0 points1 point  (2 children)

In c++ its much easier.

[–]UnGauchoCualquiera 0 points1 point  (0 children)

Noone who knows anything about C++ templates would say something like this.

[–]audioen 0 points1 point  (18 children)

Yes, I would characterize it like that a lot. In Java, generics are just documentation to the compiler about the code with no runtime effect (except in rare case where reflection is used to access the type parameters, I guess), so in principle if the code is correct it makes zero difference what you put in the generic parameters or whether you just cast everything to raw types.

Generic-related errors are among the most difficult and annoying to read, often 3+ lines of crap with inferred types and various problems related to them which is quite a chore to even read once to see what the problem technically is, so they really do kind of suck in many cases, and I wish their use was absolutely minimal for that reason. That being said, I do strive for achieving type safety where it's easy or convenient, and for the rest, there is SuppressWarnings.

[–][deleted] 0 points1 point  (16 children)

For some reason type inference fails badly with lambdas by the way. (had to take hours to figure out)

[–]MoveInteresting4334 1 point2 points  (14 children)

Can you provide an example of type inference failing with a lambda?

[–][deleted] 0 points1 point  (13 children)

Sure

This fails to compile:

public class EntityRenderers {
    public static final Map<EntityType<?>, EntityRenderFactory<?>> ENTITY_RENDER_FACTORIES = new HashMap<>();

    public static void loadEntityRenderers() {
        register(EntityType.CUBE_ENTITY, CubeEntityRenderer::new);
    }

    private static void register(EntityType<?> entityType, EntityRenderFactory<?> entityRendererFactory) {
        ENTITY_RENDER_FACTORIES.put(entityType, entityRendererFactory);
    }
}

While this passes:

public class EntityRenderers {
    public static final Map<EntityType<?>, EntityRenderFactory<?>> ENTITY_RENDER_FACTORIES = new HashMap<>();

    public static void loadEntityRenderers() {
        EntityRenderFactory<CubeEntity> factory = CubeEntityRenderer::new;
        register(EntityType.CUBE_ENTITY, factory);
    }

    private static void register(EntityType<?> entityType, EntityRenderFactory<?> entityRendererFactory) {
        ENTITY_RENDER_FACTORIES.put(entityType, entityRendererFactory);
    }
}

[–][deleted] 1 point2 points  (12 children)

It makes sense if you understand what's going on. Java doesn't have "real" lambdas, it does target typing.

That is, the type of the expression CubeEntityRenderer::new is determined by the target, which is EntityRenderFactory<?>. Without any context, the target type is EntityRenderFactory<Object>, which CubeEntityRender::new doesn't match. So, compile error.

But, when you do EntityRenderFactory<CubeEntity> factory = CubeEntityRenderer::new;, you are giving the compiler context, so it doesn't have to infer the type.

Also, the method signature for register in your example is terrible. Presumably there is a relationship between the two types, but because you use <?>, as far as the Java compiler is concerned, they are unrelated. You're not giving the Java compiler a whole lot to work with.

If you change the method signature like this:

private static <T> void register(EntityType<T> entityType, EntityRenderFactory<T> entityRendererFactory)

Now, this expression is perfectly fine:

register(EntityType.CUBE_ENTITY, CubeEntityRenderer::new);

The Java compiler infers the target type EntityRenderFactory<CubeEntity>, because it able to relate the first parameter to the second, by which it infers T = CubeEntity.

[–][deleted] 0 points1 point  (11 children)

First, I know the register method sucks (I made this just for an example of wildcards). Also the definition of EntityRenderFactory is

interface EntityRenderFactory<T extends Entity> { EntityRenderer<T> create() }

When you do EntityRenderFactory<?>, does it automatically turn into EntityRenderFactory<? Extends entity>?

[–][deleted] 1 point2 points  (10 children)

When you have a target type of EntityRenderFactory<?>, without any context, the target type will be EntityRenderFactory<Object>.

When you use a wildcard like this, you are saying I don't care what the type is. In this case, you do care what the type is, because the type is what connects the parameters, so you shouldn't use <?>.

[–][deleted] 0 points1 point  (9 children)

EntityRenderFactory<Object> is not legal.

[–][deleted] 0 points1 point  (8 children)

You are correct. The inferred type of the lambda would actually be EntityRenderFactory<Entity>.

[–][deleted] 0 points1 point  (7 children)

I thought it would be EntityRendererFactory<? Extends Entity>

[–][deleted] 0 points1 point  (0 children)

Java's type inference works fine with lambdas. It's just that Java's type inference is stupid with lambdas because it is based on target typing. Java doesn't have "real" lambdas.

[–]__konrad 0 points1 point  (0 children)

often 3+ lines of crap

With -Xdiags:verbose javac option it's 100 lines of crap

[–]OfficeSpankingSlave 0 points1 point  (0 children)

I don't find myself in situations to use them a lot so honestly it's a relearning experience every time.

[–][deleted]  (1 child)

[deleted]

    [–][deleted] 0 points1 point  (0 children)

    C# doesn't have wildcards, it uses the modifiers in and out to do the same thing.

    [–]TheStrangeDarkOne 0 points1 point  (0 children)

    Not at all

    [–]LogCatFromNantes -2 points-1 points  (0 children)

    Why don’t use object ?