Nuances of overloading and overriding in Java
software.rajivprab.com
software.rajivprab.com
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.
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.
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.
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.
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.
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.
> 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.
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 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.
It’s Oracle’s saber rattling lately concerning plugins like Lombok and Manifold that make me more disappointed than nervous.
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.
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.
Modern Java is the title of the series that talks about many different subjects including new Java language features, etc.
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.
It's very impressive and far bigger than the operator overloading stuff. The other videos in that series cover some of the other features.
- 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.
In PHP, surely you can use traits.
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.
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.
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.
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.
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>'AFAIK,that's the only big downside to multiple inheritance.
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...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.
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)
> 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, 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.
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).
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.
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.
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.
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.
I wonder what fraction of the HN readership gets this joke ...