There are some good examples here. The top voted answer (which was accepted) does a great job of explaining the basics, and is certainly better than many of the FlyingCar extends Car implements Flying and Car extends Vehicle and Vehicle extends ManufacturedGood examples you might get in school or textbooks.
When I first learned OOP after coming from C, it didn't really click for me either. I used classes as like C structs: just a way to lump variables together. I also had accessor methods, because you're supposed to, all books shoed them. At first, they just seemed like extra typing instead of using public properties.
As time went on I started adding other methods to my classes, and objects become "whole" instead of special structs. Bundling the code together makes maintenance and trying to understand the code easier. It cleans up your code quite a bit to be able to write
myThing.doBehavior(1, 3, false);
someThingSubclass.doBehavior(2, 7, true);
instead of
do_behavior_for_thing(&myThing, 1, 3, false);
do_behavior_for_thing_subclass(&someOtherThing, 2, 7, true);
After a while and you internalize that, you'll use objects and become comfortable. Then one day you'll use inheritance (which is the real secret power) and see just how much easier your life becomes (if done correctly). Compare putting the top and bottom code above into a loop:
Thing theThings[] = {myThing, someThingSubclass, aThirdKindOfthing};
for (int i = 0; i < theThings.length; i++)
theThings[i].doBehavior(7, 3 * i, true);
vs.
Thing theThings[] = {myThing, someThingSubclass, aThirdKindOfthing};
for (int i = 0; i < theThings.length; i++)
if (theThings[i].kind == 1)
do_behavior_for_thing(&theThings[i], 7, 3 * i, true);
else if (theThings[i].kind == 2)
de_behavior_for_thing_subclass(&theThing[i], 7, 3 * i, true);
....
The top loop is two lines. I can keep adding subclasses of Thing to my array and it works fine. In the bottom loop, every time I need to handle a new kind of object, I need to edit the if statements.
Game example
It makes it easy to make small changes to existing objects. Let's take a game, something like Robotron or Geometry Wars where enemies might chase the player. Think of an enemy who blindly chases the user's current position, we'll call him Zombie.
What if you want to add a second, harder enemy, called SmartZombie? You could just copy the Zombie class and edit it, but that would leave you with a bunch of duplicate code. But why not change the Zombie class from something like this:
public class Zombie {
public NextMove makeNextMove(GameState game) {
TargetPos pos = game.getCurrentUserPosition();
// process from there
}
}
to
public class Zombie {
protected TargetPos getTargetPos(GameState game) {
return game.getCurrentUserPosition();
}
public NextMove makeNextMove(GameState game) {
TargetPos pos = getTargetPos(game);
// process from there
}
}
With that little change, you can make your new Zombie chase the user's predicted position by overriding one little method:
public class SmartZombie extends Zombie {
protected TargetPos getTargetPos(GameState game) {
// Calculate where the user will be by the time we could catch him
}
}
Now, when the game asks the Zombie class for his next move, he will walk to where the user is. But if the game is given a SmartZombie instead, without having to know that something different needs to happen, the SmartZombie's makeNextMove function will be called (inherited from the Zombie class). makeNextMove will call the SmartZombie's getTargetPos (since we overrode the Zombie's method) and instead go towards the user's predicted position.
You can use this to easily make other zombies. You could make one that goes towards a random place, one that follows other zombies, all sorts of things. Each of these is a simple change with the above model, letting you have only one copy of the makeNextMove function (which could be really long and involved because it uses A* pathfinding, terrain detection, random staggering, trap avoidance, and other fancy things).
You don't even need to setup the getTargetPos when you first write the class. If you don't know you'll need to access it later, you don't need to make a method for it. When the time comes that you do need to make it easy to change some piece of functionality, that's when you can edit the Zombie class to put the part you need to be able to change into a small function you can easily override.
In my daily work
The system I spend my time on has to interface with many external systems. The basic operations are the same, but the way we communicate can be wildly different (XML, HTTP, other). Even individual implementations can be very different (between two XML interfaces).
But we have a class somewhat like this:
public abstract class abstactSystemInterfacer {
public ReturnValue contactOtherSystem(InputData data);
public boolean interpretResponse(ReturnValue value);
public Status checkWithOtherSystem(Thingy thing);
protected void helpfulMethodForSubclasses(....);
}
The code in the heart of the program is given a subclass and just calls contactOtherSystem and other methods it needs. It doesn't need to know if we're using XML to talk to the other system. It doesn't know at all.
So we can new code without having to change the main loop. It doesn't need to have a new block of an if or switch statement added when someone new appears. If we have to work with someone new tomorrow who uses some method we've never used before (such as putting weird files in special FTP directories), we can do it without having planned for it before.
This kind of thing makes your life very easy, and the core code very clean. It's up to the subclasses to handle all the details. If you are making a game, you can make enemies this way. Each one implements their behavior in a function, and the game engine calls it. When you make a new kind of enemy, it's as simple as making a new class and instantiating it.
Over time, as you get tons and tons of classes, things can get a little unwieldy. When that happens, there is often commonalities. So instead of every SystemInterfacer subclass being a direct subclass, you can make a new class called AbstractXMLSystemInterfacer. All the XML SystemInterfacers subclass that. Since it contains code that is common to all the XML systems you deal with, you can save duplication and prevent bugs.
When you have something that matches well to objects and inheritance, it's a great tool. It can make your life SO much easier.
OOP isn't always necessary
As I said, not everything needs OOP. Small programs can easily be made that wouldn't benefit from inheritance. I often write little tools to make automate some little process. These are usually one file, a few functions. The programs could even be much bigger. But if they only ever process one kind of "thing", OOP doesn't really add much over procedural code. Don't worry if you don't see how to make all your programs OOP... they just might not fit the OOP mould.
Conclusion
It took me a while to 'get' OOP because I tried to teach it to myself. I learned programming from books and tinkering before I could take programming classes, in C and Basic. The idea of objects and OOP was pretty foreign from how I was used to doing things, and I didn't have a lot of experience in the fist place. It wasn't until I started doing some non-trivial programming assignments (more than a few functions and one or two files) that asked me to use OOP that I really had a chance, and the utility of it really clicked.
Once I had a reason to use it, I got it pretty fast. As you use it in more and more assignments, you'll get it too. From my experience, and those of my classmates in school, it's not one of those things you can just read about and start applying immediately. It's a bit of a different, sort of a mindset or way of thinking.