What I consider a mistake is not making override a proper keyword.
Interesting. Could you elaborate on why you find that so?
By way of illustration regarding why I ask: I've been writing Java code for 20+ years now, and I can't remember a single time that the "default virtual" behavior wasn't what I wanted. And I wrote C++ before moving to Java so I'm familiar with the approach of requiring one to mark methods as virtual explicitly. I've just always found that the Java approach makes sense. What am I missing?
The reasoning goes stepwise.
1. In a good design, a class interface represents an abstraction. The public interface provides access at the level of the abstraction, obscuring details of implementation.
2. If it is a good abstraction, by definition it maps the public view to an internal model that differs from the public view.
3. Inheritors provide different implementations of the abstraction by defining their own versions of inherited virtual-function signatures, operating on the internal, abstracted model.
4. Because these virtual functions operate on the implementation, they should not be exposed to the client. Exposing them would violate the abstraction, exposing the internal model.
5. The public members of the base class translate abstract operations to operations on the internal model, and call virtual functions that provide variable concrete operations on it.
6. Therefore, it is wrong for the public members of the base class to be virtual: their interface is the public view of the abstraction. It is their job to map that view to the internal model, and to call the virtual functions that implement the model.
7. Similarly, it is wrong for the virtual functions to be public, because they implement the internal model. Making them public would expose it.
All that said, since Java offers only one organizing mechanism, the class, it necessarily gets used for everything, and not just for well-designed abstractions. It is not wrong to treat language features as a pot of mechanisms, and use them in ways that do not match their designed intent. Coding Java, there is no choice about it; classes are all you have, so you use them for everything, and the well-designed abstraction may be a rarity among all the other uses, and might not exist at all in many programs.
But Java's limited feature set is out of scope for discussion of the designed purpose of virtual functions. Used according to the Object Oriented model, public virtual functions are an oxymoron. But as mechanism, they are as usable as any other, if they work.
It would be a huge mistake in a statically compiled language because the static compiler doesn't have as much information as a JIT in flight.
One downside is that every method now requires following a pointer, which is pretty awful for performance on modern hardware.
Java makes a lot of common classes final (e.g. String and Optional) so that it can avoid this. As I understand it, it's also got a lot of pixie dust to predict this some of the time, but then you can't rely on it.
Final classes encourage the creation of utility classes, which can often become a problem.
There are two different ways to create a subclass in OOP. Java treats the subclass and the superclass as the same object. If the subclass calls a method on itself, it may go to the superclass first. This is a valid way to do things, but it can be dangerous and confusing.
Another way to create a subclass is to create two objects: the superclass and the subclass. The subclass has a reference to the superclass, but they are not the same object. If the subclass doesn't override one of the superclass's methods, then it implicitly proxies the method call to the superclass. If the superclass calls a method on itself, then it goes to itself rather than the subclass.
The difference between the two approaches is whether you want your class implementation to be open to extension (monkey-patching). Allowing class inheritance (Java's approach with the virtual methods) means that subclasses can override methods you define. It can be convenient, but it can also be a foot-gun. Class composition (proxies) prevents monkey-patching by subclasses.
This seems like some C++ pattern holdover where you obviously do have virtual vs non-virtual methods. 99% of the time I suspect it's just a source of extra bugs and the vtable lookup really isn't the performance hit (most of the time) you think it is.
I don't think it's that big a deal however because, in practice, libraries that use inheritance as an external API are almost an anti-pattern at this point. I forget who said it but someone (Josh Bloch? Scott Meyer?) said at one point "inheritance is an implementation detail". Or maybe it was "inheritance breaks abstraction"? Something like that anyway.
I 100% agree with this. At that point it doesn't really matter if your methods are final or not. Ideally you make your leaf classes final and move on with your life. But designing for inheritance is probably not going to end well regardless other than some notable utility classes (eg AbstractMap).
So instead of just setting up and executing the call, you have to traipse through memory a bunch of times, affecting caches and making things slower.
But the thing is, Java's compiler works at runtime, so it can either inline the function (basically dump the code into the call site instead of invoking) or devirtualize it (if the same object is called over and over, the JIT can just remember the last lookup and add a guard if the target changes).
In say C++, making everything virtual would be crazy because the compiler can only safely inline in certain situations and can rarely devirtualize a call.