So long as you use
getType() or anything like it, you're not using polymorphism.
I understand feeling like you need to know what type you have. But any work you would want to do while knowing that should really be pushed down into the class. Then you just tell it when to do it.
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.
— Alec Sharp
This principle is called tell, don't ask. Following it helps you not spread details like type around and create logic that acts on them. Doing that turns a class inside out. It's better to keep that behavior inside the class so it can change when the class changes.
You can tell me no other shapes will ever be needed but I don't believe you and neither should you.
A nice effect of following encapsulation is that it's easy to add new types because their details don't spread out into the code where they show up in
switch logic. The code for a new type should all be in one place.
A type ignorant collision detection system
Let me show you how I'd design a collision detection system that is performant and works with any 2D shape by not caring about the type.
Say you were supposed to draw that. Seems simple. It's all circles. It's tempting to create a circle class that understands collisions. The problem is this sends us down a line of thinking that falls apart when we need 1000 circles.
We shouldn't be thinking about circles. We should be thinking about pixels.
What if I told you that the same code you use to draw these guys is what you can use to detect when they touch or even which ones the user is clicking on.
Here I've drawn each circle with a unique color (if your eyes are good enough to see the black outline, just ignore that). This means every pixel in this hidden image maps back to what drew it. A hashmap takes care of that nicely. You can actually do polymorphism this way.
This image you never have to show to the user. You create it with the same code that drew the first one. Just with different colors.
When the user clicks on a circle I know exactly which circle because only one circle is that color.
When I draw a circle on top of another I can quickly read every pixel I'm about to overwrite by dumping them in a set. When I'm done the set points to every circle it collided with and now I only have to call each one once to notify it of the collision.
A new type: Rectangles
This was all done with circles but I ask you: would it work any different with rectangles?
No circle knowledge has leaked into the detection system. It doesn't care about radius, circumference, or the center point. It cares about pixels and color.
The only part of this collision system that needs to be pushed down into the individual shapes is a unique color. Other than that the shapes can just think about drawing their shapes. It's what they're good at anyway.
Now when you write the collision logic you don't care what subtype you have. You tell it to collide and it tells you what it found under the shape it's pretending to draw. No need to know type. And that means you can add as many sub types as you like without having to update the code in other classes.
Really, it doesn't need to be a unique color. It could be actual object references and save a level of indirection. But those wouldn't look as nice when drawn in this answer.
This is just one implementation example. There certainly are others. What this was meant to show is that the closer you let these shape subtypes stick with their single responsibility the better the whole system works. There likely are faster and less memory intensive solutions but if they force me to spread knowledge of the subtypes around I'd be loath to use them even with the performance gains. I wouldn't use them unless I clearly needed them.
Up till now I've completely ignored double dispatch. I did that because I could. So long as the colliding logic doesn't care which two types collided you don't need it. If you don't need it, don't use it. If you think you might need it, put off dealing with it as long as you can. This attitude is called YAGNI.
If you decide you really need different kinds of collisions then ask your self if n shape subtypes really need n2 kinds of collisions. So far I've worked really hard to make it easy to add another shape subtype. I don't want to spoil it with a double dispatch implementation that forces circles to know that squares exist.
How many kinds of collisions are there anyway? A little speculating (a dangerous thing) invents elastic collisions (bouncy), inelastic (sticky), energetic (explody), and destructive (damagy). There could be more but if this is less than n2 lets not over design our collisions.
This means when my torpedo hits something that accepts damage it doesn't have to KNOW it hit a space ship. It only has to tell it, "Ha ha! You took 5 points of damage."
Things that deal damage send out damage messages to things that accept damage messages. Done that way you can add new shapes without telling the other shapes about the new shape. You only end up spreading around new types of collisions.
The space ship can send back to the torp "Ha ha! You took 100 points of damage." as well as "You're now stuck to my hull". And the torp can send back "Well, I'm done for so forget about me".
At no point does either know exactly what each is. They just know how to talk to each other through a collision interface.
Now sure, double dispatch lets you control things more intimately than this but do you really want that?
If you do please at least think about doing double dispatch through abstractions of what kinds of collisions a shape accepts and not on the actual shape implementation. Also, collision behavior is something you can inject as a dependency and delegate to that dependency.
Performance is always critical. But that doesn't mean it's always an issue. Test performance. Don't just speculate. Sacrificing everything else in the name of performance usually doesn't lead to performent code anyway.