Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Nuances of overloading and overriding in Java (rajivprab.com)
87 points by whack on May 18, 2023 | hide | past | favorite | 74 comments


Varargs methods in Java was one that bit me with an overload in the past (my tale of is told in more detail at http://the-whiteboard.github.io/java/debugging/warstory/2016... )

The issue was that there was a framework class with the method signature of execute(Object ... inParams). The class had overloaded the method signature to be execute(String foo, String bar). If you called it with two args, it got the overloaded call and there were a half dozen calls to this method.

At some point, someone added another argument to this so that it was execute(String foo, String bar, String qux) and updated the half dozen less one methods to use the third parameter.

However, the two args call in some other code still compiled correctly because it now went back to calling execute(Object ... inParams). Though, the parameters were completely wrong for that call structure and the application suddenly sprouted runtime errors.


This kind of thing is exactly why in C++, when a derived class declares a method, it doesn't just overload any methods with the same name inherited from the bases, but hides them. If you actually want overloading across the hierarchy, the derived class needs to explicitly opt into it by "using" the base methods it needs.


In fairness, it was pretty reckless to just "update" the existing method without providing a dummy "throw an informative error if anything attempts to call the old method" fallback method


There were all sorts of "wrong" with that attempted refactoring that goes back to the original "subclass the StoredProcedure class and overload the execute(Object ...) method rather than creating a new one... and name it callProc (for example).

Note that the execute methods ( https://docs.spring.io/spring-framework/docs/current/javadoc... ) all are suggested for subclass invocation rather than external.

I'd have to spin up a test to verify, but I believe that if Spring had made it a protected method so that execute could only be called by a subclass rather than public where it can be called from other methods in other classes it would have mitigated the issue that allowed for the original developer to overload it. That would have made it so when the arguments changed it now was a compile error since the execute(Object...) form of the call would only have been accessible from the subclass.


Most of these rules are pretty straigthtforward. I don't really find any surprises in the Java method dispatch system, it just sort of works without a lot of thinking about it.

The one area I think leaves some question marks for me is the "new" default methods in interfaces, aka "public defender methods".

They were introduced awhile back, but they've mostly been avoided until recent years. As they become more common, a thorough understand of how Java deals with double-diamond needs to be understood by Java developers.


Many of these things are at the core of the book Java Puzzlers.

http://www.javapuzzlers.com/


None of this is surprising. Just think about how the compiler is going to use the object's vtable. There is no magic going on.


I think the main confusion is that when you override a method in Java, similar to other languages like C++, it considers not only the name but actually the entire function signature. That's in fact why overloading works to begin with, because of the fact that the identity of a method is not just its name but in fact it's signature.


I believe that OP is confused about when the resolution happens. Overloading is resolved by the compiler, inheritance at runtime


Kind of a blurry distinction with a JIT-compiled languague tbh.


why? the JIT works on bytecode and JVM bytecode has no concept of overloading. Maybe I should have specified that by "compiler" i meant the first compilation step, Java source to bytecode.


The JVM bytecode is not the JVM.

There’s some late binding allowed in the JVM to deal with API changes. So the caller can call a method signature that doesn’t exist in the target (but either does in a later version or did in a previous one), and the JVM has to resolve it at first invocation, rewriting the bytecode in the process.

The compile time resolution ends up being a hint that usually works, but doesn’t always. And if memory serves a number of languages that run on top of the JVM have leveraged this fact, including the original third party implementation of generics.


I'm having trouble finding the behavior you describe in the JVM specification. Except for the special case of signature polymorphic methods, a resolved method must always match both the name and descriptor, as far as I can tell. You linked the invokespecial opcode, but that just talks about how to search through superclasses and superinterfaces to find the target method.


still the lookup is based on the symbol, no? so the JVM does not resolve overload even in that case, i believe


Read what I said again.

Step 1 is a lookup by symbol. You're ignoring the vast majority of the logic.

There are hints about what I'm talking about in this conversation about invokespecial: https://stackoverflow.com/questions/13764238/why-invokespeci...

This trickiness is not called out in the bytecode specification, but in the JVM spec. They get a little closer to the mark here:

https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-6.ht...

"Is Java Statically Typed or Dynamically Typed?" is basically an interview gotcha question. Most people should answer the former, but if you are hiring people to create a JVM or a language that translates to the JVM, you're looking for a much longer answer that circles around gradations of the latter, because the compiler is strongly typed but the JVM has weaker type guarantees that are sorted out at first invocation, and which look like variance (predating most literature on contra and covariance, I might add). Prior to invokedynamic people used this and other facts to trick the JVM into doing things that are illegal in Java. The Hotspot people learned a whole lot about how the JVM actually functions from interactions with these sorts of people.


i now get what you mean, thanks


If I wasn't already clear, this isn't something that normal devs should have or want to think about.

But it's hard to avoid if you end up dealing with separate compilation units that are run on a different build schedule. Sooner or later your refactoring will either blow up with an obscure error, or work even though if you really think about it, the compiler would say it shouldn't. And of course if you actually dig you'll find it out as well.

If you want sophisticated dispatch you want to use invokedynamic, but that was an RFE when I was learning about this stuff.


I rather suspect that "magic" and "vtable" are very similar concepts to many developers.


I'm disappointed that

> The method that gets invoked depends on the “actual” instance type, not the “declared” instance type.

doesn't apply more broadly. If you have a call(fruit) method and a call(apple) method, when you supply a fruit, it will resolve to call(fruit), even if the fruit instance is actually an apple.



Thanks for this curation, useful for me as a beginner. All the comments belittling these puzzles kinda feel rude.


Are these things HN readers didn't know?

I didn't know you can do operator overloading in Java (with Manifold). That would be interesting: https://www.youtube.com/watch?v=pwQs-308OdY


I would be terrified on introducing that into a project and worrying about its long-term support...


It is interesting through ;-)

Personally, I will probably introduce some of it to a project I'm working on. Here's why:

* It's relatively conservative - almost of all its work is done in compile time. Once you have a binary and it went through testing then Manifold is irrelevant.

* It's open source - I know it's not the same as "support" but the project has been around for a while and the author is active.

* Worst case scenario I can remove it - it would suck since I will need to rewrite code. But the compiler will point me at the exact lines of code I need to fix.


A team I was on decided against Manifold precisely because we wanted to keep up with Java's twice-yearly version updates, and we didn't want a bespoke compiler plugin to lock us to an older version.

I think Manifold is extremely cool, and it does look like they're keeping up with the latest -- but the deep compiler magic it relies on really makes me nervous.


They are good about supporting latest JDK release and all LTS releases, going back to 8.

It’s Oracle’s saber rattling lately concerning plugins like Lombok and Manifold that make me more disappointed than nervous.


Yes, exactly -- the responsibility to keep up with Java versions does not, unfortunately, lie with Manifold alone. The deep magic they rely on also needs to remain accessible.


What rattling is that?


I was not aware of manifold; from a quick look, my understanding is that manifold is a bunch of popular language extensions, implemented mostly via preprocessor annotations, which are highly tied to specific java versions and IDEs.

But while that does seem cool, I'd hesitate to call manifold-driven operator overloading as "modern java supporting op overloading". To me it has the same vibe as simulating high-level syntax in c via hacky macros.


That's not how it works exactly. It's a deep integration as a compiler plugin extension. It works with LTS versions since 8 and with Java 19 (I think 20 also works but not sure).

Modern Java is the title of the series that talks about many different subjects including new Java language features, etc.


As in, the java compiler formally provides extension APIs, and Manifold taps directly into those?

Hm, ok, in that case, slightly less hacky. Probably more comparable to using gcc extensions then... in that, I'd still hesitate to call it "this is evidence that c supports x" ... but it's the kind of statement that could be argued both ways, and discussions would probably quickly devolve into semantics and pedantry rather than anything of practical significance.


Yes. I suggest checking out the full project source: https://github.com/manifold-systems/manifold

It's very impressive and far bigger than the operator overloading stuff. The other videos in that series cover some of the other features.


Reminds me of CLOS and the versatility of defgeneric and defmethod.


Makes you appreciate the complexity of haskell.


I hate inheritance.


Some months ago, I stumbled into an OOP inheritance rabbithole which got me thinking the same:

- Isn't the (biological) concept of inheritance built at it's core around the idea of "generations"? How does that make any sense in OOP?

- Why do Java beginner courses still teach Inheritance like it was 1995 with all the bells and whistles and dogs and cats and mammals?

- Why do they teach Inheritance and later introduce "favor composition to inheritance"? Doesn't this just confuse everyone?

- Why does Inheritance have first-class syntax support in an OOP language like Java ("extends") whereas Composition usually needs to be "engineered" using some more complex patterns, e.g. DI?

Most of it is probably for historical reasons, i guess.


"Why does Inheritance have first-class syntax support in an OOP language like Java ("extends") whereas Composition usually needs to be "engineered" using some more complex patterns, e.g. DI?"

I think the main reason is that DI is seen as an orthogonal concept by language designers, so you'd need new two first-class features in a language.

If you do composition without injection (e.g.: by having new ChildObject() in the constructor), you don't really require that many more lines of code compared to inheritance.

    class Car {
        private Engine engine;

        public Car() {
            engine = new Engine();
        }

        public void drive() {
            engine.start();
            System.out.println("Driving...");
            engine.stop();
        }
    }
Of course it's much less flexible and less testable than composition + injection, but not that inflexible when compared to inheritance. And first-class support for only that would make the feature a bit useless without DI...


> How does that make any sense in OOP?

OOP inheritance forms a tree, and that is the language of trees (parent, child, ancestor, descendant, sibling).

> Why do Java beginner courses still teach Inheritance like it was 1995

> Why do they teach Inheritance and later introduce "favor composition to inheritance"? Doesn't this just confuse everyone?

Codecademy's course introduces inheritance in module 9 of 11, and calls it "deeper object-oriented" feature: https://www.codecademy.com/learn/learn-java

> Why does Inheritance have first-class syntax support in an OOP language like Java ("extends") whereas Composition usually needs to be "engineered" using some more complex patterns, e.g. DI?

Class members absolutely have first class syntax.

DI is composition with IoC container.


Yep. You've got to remember that Java is nearly 30 years old; at the time making a distinction between interfaces and abstract classes was seen as a radical move. Newer languages have better support for more decoupled ways of doing things, e.g. Rust's use of typeclasses (which it calls traits for some reason) or Kotlin's built-in support for delegation.


> Rust's use of typeclasses (which it calls traits for some reason)

Scala and (apparently) PHP call them traits too, among many others [1] -- Rust wasn't the first here.

[1] https://en.wikipedia.org/wiki/Trait_(computer_programming)


The thing that Rust calls traits is quite different from the thing that Scala and PHP call traits, and much more like the thing that many languages call typeclasses.


If every class inherits Object then you probably need to understand the concept still, even if it’s no longer favored for organizing your own business logic. Besides, you will encounter other people’s code that doesn’t follow all the best practices eventually. And I’m not sure anyone has come up with a better way of teaching inheritance in the past few decades.


I don't know for Java currently but for example in Ruby composition is simply a matter of putting your code in a module rather than a class. Then you can extend any class with this module. You can even extend instances. That is 3.extends(Some_module).method_from_some_module is perfectly valid.

In PHP, surely you can use traits.


Of the many models of “composition” that are possible I think Ruby’s free-for-all blend of mix-ins and monkey patches is the only one that can drive a maintenance programmer more insane than a deep hierarchy of inheritance of Java classes.


    irb(main):001:0> "bar".foo
    Traceback (most recent call last):
            4: from /usr/bin/irb:23:in `<main>'
            3: from /usr/bin/irb:23:in `load'
            2: from /Library/Ruby/Gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
            1: from (irb):1
    NoMethodError (undefined method `foo' for "bar":String)
    Did you mean?  for
    irb(main):002:0> class String
    irb(main):003:1> def foo
    irb(main):004:2> "foobar!"
    irb(main):005:2> end
    irb(main):006:1> end
    => :foo
    irb(main):007:0> "bar".foo
    => "foobar!"
    irb(main):008:0> 
Yes! Let's modify the core library String class on the fly to add new functions to it.

There are things about ruby that truly scare me if my goal was to write secure and reasonable code.


This doesn’t leverage on the more subtle but all the more elegant type system that Ruby provides. As told just above, since all object also have their own class instance you can extend this instance specific class without polluting the general class it derives from.

  irb(main):001:1* module Awesomeness
  irb(main):002:1*   def awesome? = :yes
  irb(main):003:0> end

  => :awesome?
  irb(main):004:0> ?Ô.awesome?
  (irb):4:in `<main>': undefined method `awesome?' for "Ô":String (NoMethodError)
          from /Users/someone/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
          from /Users/someone/.asdf/installs/ruby/3.1.2/bin/irb:25:in `load'
          from /Users/someone/.asdf/installs/ruby/3.1.2/bin/irb:25:in `<main>'

  irb(main):005:0> ?Ô.extend(Awesomeness).awesome?
  => :yes

  irb(main):005:0> ?Ô.awesome?
  (irb):5:in `<main>': undefined method `awesome?' for "Ô":String (NoMethodError)
          from /Users/someone/.asdf/installs/ruby/3.1.2/lib/ruby/gems/3.1.0/gems/irb-1.4.1/exe/irb:11:in `<top (required)>'
          from /Users/someone/.asdf/installs/ruby/3.1.2/bin/irb:25:in `load'
          from /Users/someone/.asdf/installs/ruby/3.1.2/bin/irb:25:in `<main>'


Right, that’s monkey patching. You run into the real problems when you have multiple people wanting to add them.

Ruby isn’t alone in supporting this functionality (you can fiddle with JavaScript prototypes, for instance), but I think it is unique how much it is encouraged (at least in Rails world). I think extension methods are a much better model to achieve something similar.


There are also some really weird/powerful/voodoo things that you can do when that is combined with the first class environments.

The "why Scheme didn't do this": First-class environments - http://funcall.blogspot.com/2009/09/first-class-environments...

why Ruby did (and wasn't a good idea): Ruby Conf 2011 Keeping Ruby Reasonable https://youtu.be/vbX5BVCKiNs

Consider where you can slip in a block invocation and the following code:

    def mal(&block)
        block.call
        block.binding.eval('a = 43')
    end

    a = 42
    puts a
    mal do 
      puts 1
    end
    puts a
and that that means that you've got full access to be able to inspect and manipulate all of the variables in scope at the time of the invocation. While that example is rather obvious, it can be done much more subtly too.

The power of ruby to do meta programming and by extension some really neat DSLs also provides it with some dangerous tools that are otherwise rather difficult to track down.


This is the Smalltalkiness of Ruby. Why have a separate place to put a function that operates only on strings when you can put it in String?

Of course, in Smalltalk you might have #trim defined by two different packages and they might conflict. Then what? Maybe you have to manage that complexity. However, it's not much different from having conflicting functions in the global namespace in C, but actually easier to resolve in a given situation.


Well, I suppose that's compelling if you're one of the many programmers trying to decide if you will implement your project in Ruby or C.


Yeah, more modern Smalltalks are able to put things in various namespaces so they don't conflict but still generally work as expected. In Smalltalk, adding methods doesn't weigh down a class like in other languages, because method lookup is on-the-fly like in Objective-C, not precalculated and placed in a table.


I really liked all these questions, thanks for sharing. I'll think about them.


I remember when C++ was touted as a superior language to Java because C++ has support for multiple inheritance.


Java has interfaces with method implementations now. You can do multiple inheritance with it too :P


they did avoid the diverging diamond of death by making it a compiler error to have duplicate functions though.

AFAIK,that's the only big downside to multiple inheritance.


Because it's an object-oriented language, and so its primary paradigm is object-orientation. Various style guides will tell you that object-orientation is bad and you should try writing in a procedural style instead, but the correct answer when you pine for procedural programming is to stop using Java.


Why do we need DI to declare that a class has a member?


Just say no.


These are one of the things that shiver me in production legacy code.


"half a decade" isn't a brag. Its overselling the fact that you coded for 5 years. Big whoop. Stop being a wordcel and making it seem like you are more qualified than others in the space because you used the term "half a decade" like its some large quantity of time.


A good reason, if any were needed, to escape from the Java OOP maze once and for all and never return. Honestly, talk about a mountain of incidental complexity. People, wake up! It doesn't have to be this way. You've been sold Java's absurd contortions dressed in the emperor's new clothes as the default by universities and big corps for decades. There's a whole other world out there where data is .... just ..... data.


Between duck-typing languages like python and javascript and the minefiled that is C++, I think Java and C# fit a pretty nice sweet-spot. Java absolutely has "just data", they're mostly used for DTOs and database Entities, with tools like lombok @Data and `records` being some nice mechanisms of implementing them.

Inheritance, when used for implementing is-a relationship, is a pretty powerful tool. If you're surprised by overriding, maybe instead of complaining about `Parent parent = new Child()` examples, try to see a `Writer writer = new PdfWriter()`. Are you still surprised that `writer.write(document)` generates a pdf?

There are absolutely horrible ways of designing code in Java, but that can be said about any language.


> Between duck-typing languages like python and javascript and the minefiled that is C++, I think Java and C# fit a pretty nice sweet-spot.

Imagine where we would be if an actually good way of doing type systems had been invented in the 1970s and there was a whole family of languages that followed that approach. Oh wait.

> Java absolutely has "just data", they're mostly used for DTOs and database Entities, with tools like lombok @Data and `records` being some nice mechanisms of implementing them.

The fact that you have to step outside the language with @Data rather proves the point. And while Java now has a "record" keyword, they're still reference types, with turning them into actual values being something that's supposedly coming Real Soon Now for 10+ years.

> Inheritance, when used for implementing is-a relationship, is a pretty powerful tool. If you're surprised by overriding, maybe instead of complaining about `Parent parent = new Child()` examples, try to see a `Writer writer = new PdfWriter()`. Are you still surprised that `writer.write(document)` generates a pdf?

Inheritance is a conflation of 3 useful features (interfaces, composition, and delegation) that's a lot less useful than the sum of its parts. Better languages separate these out properly. (And, credit where it's due, Java made a positive step in normalizing the separation of interfaces from the other parts of inheritance).

> There are absolutely horrible ways of designing code in Java, but that can be said about any language.

That's a cop-out. No language completely eliminates bad code, but there are better and worse languages.


> The fact that you have to step outside the language with @Data rather proves the point.

You can trivially write a class with public fields without using lombok, you wont get some cookie cutter logic for hashCode and equals, but often you don't need it and with mutable data you may not even want it.


So how would you model a GUI framework’s Node type in (I assume ML/Haskell’s type system is what you mean)? Inheritance is rarely the answer, but it is more than the sum of its parts, and in the rare case it is needed, you really can’t go around that.


> So how would you model a GUI framework’s Node type in (I assume ML/Haskell’s type system is what you mean)?

You'll have to be more specific.

> Inheritance is rarely the answer, but it is more than the sum of its parts, and in the rare case it is needed, you really can’t go around that.

Disagree. If you have the individual parts - interfaces, composition, and delegation - then you can do everything that you can do with inheritance, and you can usually do better since most of the time you only need one or two of the three.


> 1970s

I wonder what fraction of the HN readership gets this joke ...


That's a good point. But IME, when people complain about inheritance, they're primarily talking about implementation inheritance.

Interface inheritance, used for polymorphism like in your example, is still considered a good practice and is not really criticized as much (an exception is when it is "abused" for other reasons, like in codebases that have one interface for every class, even without polymorphism).


Eh, the meme has gotten so strong that I now see people say that subtyping in general is bad. It has escaped the gravity of concrete criticisms of implementation inheritance and morphed into a general “oop bad” thought system.


This is an uncharitable interpretation of the criticism. Several other programming paradigms also have subtyping, often more powerful than OOP, so people moving away from OOP are definitely doing some sort of subtyping in their own language and paradigm, unless they're going to some niche language... or Go.

OOP criticism is often centered around two things: inheritance and state hiding via encapsulation. And this criticism often comes also from programmers that use OOP. And IMO it is pretty fair.


GUI widgets are one area where implementation inheritance is the best model.


Most of these exact same issues would be the same in C++, for sure similarly contrived examples could be contrived.

> People, wake up! It doesn't have to be this way. You've been sold Java's absurd contortions dressed in the emperor's new clothes as the default by universities and big corps for decades.

I think this is a very incorrect characterization. Effective Java was required reading for me in the very late 90s (decades ago). Back then it was even argued favor composition over inheritance [1]. AFAIK you'd have to go back to the 70s or 80s for inheritance to still have a good reputation (and which at the time was certainly better than a GOTO statement). The incidental complexity was later revealed to be quite profound, and a large chunk of "Effective Java" is dedicated to explaining these complexities and why and how to avoid these.

[1] https://blogs.oracle.com/javamagazine/post/java-inheritance-...


Inheritance is still absolutely a good thing to leverage polymorphism - but for that all you need are simple, shallow inheritance hierarchies. What has a deservedly gotten a bad reputation are ivory tower architectures with deep inheritance hierarchies that lead to things like the Abstract Factory Pattern.


Abstract factory pattern isn't related to deep inheritance hierarchies.




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

Search: