Encapsulation is a technique that helps information hiding. Encapsulation generally means access to data fields of objects and classes has been restricted - to classes or objects of the same type as the class the field is in, or sometimes including those of subclasses as well.
Note I can do away with some but not all of the benefits of encapsulation of a "private" field if I provide a getWhatever and a setWhatever method for each whatever field. At least I can still do cool things like be able to set a breakpoint or insert a logging statement/call which runs each time another class changes the field value. However, once they get that field's value, I can't tell if they will pass it around or let someplace else - anyplace else - in the software look at it, and perhaps change it.
Providing encapsulation encapsulation of all of your fields - and some of your methods - is often a good idea. It is usually even better to hide information about those encapsulated fields as well.
Encapsulation is a coding trick and is usually supported directly by a programming language. Information hiding is something you enforce with your design and is part of your programming style - or is not.
Abstraction is a different concept. Abstraction benefits from the use of encapsulation.
Abstraction makes use of the concept of "generalization" and the principle of "specialization".
To harness the power of abstraction, you pick a broad category of thing that different types of things in your application or system belong to. Let me give an example.
Maybe your system is a simulation. Lets say it simulates wolves and rabbits.
When rabbits get hungry, they stay in one place if there is grass there, and they chew it. If there is no grass there, they look for it, until they find it. If they see a wolf, they run and hide. If the fox finds them, they run and hide again. If a rabbit gets hungry while it is outrunning a fox, it doesn't matter - it ignores its hungry because not getting eaten by the fox is a higher priority.
When fox gets hungry, and it doesn't have a dead rabbit handy, it goes off and hunts for one. When it sees it, it chases it. If it gets away, it might wait a while to see if it comes out of cover or tries to detect its sent, but eventually it gives up if it doesn't find it.
To implement an object oriented program that implemented this, you could have an abstract Animal class that has a hungry flag. It could have an act() method that says:
IF hungry
IF foodHere()
eatFood()
ELSE
searchForFood()
Then you can create a Wolf subclass of Animal and a Rabbit subclass of Animal.
A rabbit's definition of the foodHere() method is going to check and see if there is grass nearby. A wolf's version of the method will check and see if there is a dead rabbit nearby.
A rabbit's version of searchForFood will go off and look for food. Unlike a wolf, a rabbit is not going to search the same place for grass that he found no grass earlier that same day - grass doesn't grow that fast. A rabbit will try to search as wide an areas as possible, if he runs out of grass nearby. If memory doesn't tell him where he saw some recently, he could look for it near running water, or he could do a spiral search. I do not know which of these techniques he uses, I am just illustrating that his need for different food demands a different way of looking for it than food.
A wolf on the other hand is seeking animal prey, not vegetation, and prey moves around - unlike vegetation. So a wolf will likely look in places where he has spotted and/or caught prey before. If he spots a place that has live grass and there is no live grass anywhere else, he will check there. Note, again, I am not a wolf, I am just giving this as an illustration - don't take it as fact that wolfs work exactly this way!
If a wolf smells the scent of a rabbit, he will follow the scent. Likewise, rabbit droppings' scent may lead it to rabbits as well. A wolf searching for a rabbit will try to kill it. Once a wolf has killed a rabbit, it no longer has to search. There is food now. That is how the wolf's version of the searchForFood() method needs to work.
In the Animal class, I would declare the searchForFood() method as abstract; I would not supply a definition for the logic to place in the method. This would force all concrete subclasses - e.g. the Rabbit and Wolf classes - to declare the method _and_ define the logic for it. Any subclass of Animal that does not provide a definition for how to do searchForFood would be "abstract". In order to be "concrete" it would have to provide a definition for it.
Another word for abstraction is generalization. Superclasses are usually more general than subclasses. Subclasses are usually specializations of their superclasses.
Another example that is less interesting but often used is that of shapes. Say you want to have a system that draws two types of shapes: Circle and Square.
Your application can have a Shape class. A shape must know its position. Lets say we declare a coordinate field in our Shape called "position". We define the position field with the value of the upper left corner of its shape. There must also be a size - more about that in a moment. The Shape class is abstract. It will declare one abstract method: draw().
Next, you have to write two subclasses of Shape, Square and Circle. Both of them are concrete, not abstract. Square accepts a size which it will use as the length of each of its 4 sides. Circle accepts a size which it will use as its diameter. They will accept this size value in their constructor, as well as the shape. Thus, each shape object will get its size and position defined when it is constructed.
Each Shape subclass will declare and define a draw() method.
For Square, this might look like:
drawRect(position.x, position.y, position.x+size, position.y+size);
For Circle, this might look like:
drawOval(position.x, position.y, position.x+size, position.y+size);
The classes are very similar. The definition of each draw method is very similar but each one is different in some specific way. Here, unlike the rabbit and wolf example, that similarity is very obvious. The difference too is very obvious. One calls drawRect, the other calls drawOval. Other than that, they are identical - they even pass the same arguments.
So, those are 2 sets of examples. The first example lets you define an abstract Animal class; the second one, an abstract Shape class.
Rabbit and Wolf are recognizably concrete things. You can visualize a rabbit and a wolf. Likewise, in a program, the rabbit and wolf objects could be constructed.
You cannot visualize an "animal" per se. Only specific types that are specific types of animal. Likewise, a program could not construct an animal object; it lacks a definition for searchForFood(). So the Animal class is abstract. It is suitable only as a template from which other types of Animal classes can be created. Wolf and Rabbit on the other hand are concrete. They can serve as templates for creating objects of their type.
Note that were squares, circles, rabbits, and wolves in the same program - there would be no reason for the code for a square class to look inside a rabbit and see if the rabbit was hungry.
So, it is possible to encapsulate the hungry field of Animal. It is possible to give it protected access because then our Shape class and its subclasses have no good reason to examine the field or change its value. It is beneficial to do so because then we can change the Rabbit, Animal, and Wolf class - and the Shape, Circle, and Square classes will require no change.
Encapsulation enforces disciplined, minimal access of the guts of one class to another. In the absence of encapsulation, classes can become mutually dependent. That means they all wind up depending on each other's guts. In this situation, changing any field name, for instance - might make you have to change the programming of all the other classes. Not fun. And not practical in a real world computer program.
I think you mean "OOP" not "oops". You say oops after a small disaster. You do OOP to avoid one. OOP stands for Object Oriented Programming.