Fundamental Properties of Object-Oriented Programming
In this lesson, we will discuss the four fundamental properties of Object-Oriented Programming
. These are the building blocks of programming in the OOP paradigm. In fact, OOP was actually primarily invented to help programmers enforce and follow these four basic properties while writing code.
The four properties of OOP we'll cover are:
- Encapsulation
- Abstraction
- Inheritance
- Polymorphism
If a programming language supports these four properties, then it is classified as an OOP language
. Java, C++, Python, and Javascript all belong in this category. However, for a language to be considered a Pure Object Oriented Programming Language, it must follow three additional rules. Those are:
- Each primitive type must be an object.
- Each user-defined type must be an object.
- All operations on objects must be performed by invoking a method associated with an object.
Pure OOP languages have a lot of limitations in terms of practical usage which is why they are pretty rare. The only pure OOP language that is close to popular is Smalltalk.
In this lesson, we will go through the four fundamental properties one by one and give examples of each. Instead of using the keyboard-monitor example, we will use a more trivial Shape
example. We will design an object-oriented architecture to represent different kinds of shapes.

Encapsulation
This rule/property is the easiest to understand among the four. This property dictates that all objects should be capsules and that all data related to an object should be inside that object.

The code below does not follow the rule of encapsulation. An object cannot access a property if the property is outside itself (i.e., outside the capsule). Thus, this code will not even compile since encapsulation says that all objects and methods in Java must be within an object.
1// This is wrong, shapeMaxWidth is external to the class
2var shapeMaxWidth = 0;
3class Shape {
4 constructor(maxHeight) {
5 this.maxHeight = maxHeight;
6 }
7
8 getMaxArea() {
9 return this.shapeWidth*this.height;
10 }
11}
Abstraction
Abstraction
in OOP is similar to the abstract data types (ADTs) we discussed in an earlier lesson. With ADTs, we do not need to know how a data structure is implemented. We only know the specifications of how it should behave.

Abstraction is also a way to tell the user of the class how to use the class without letting them know how it is implemented internally. With abstraction, you can "hide" all the decisions and processing steps within your Keyboard
class.
Most programming languages have special keywords to utilize the concept of abstraction (e.g., public
and private
in Java). When designing a class, we can use these keywords to prevent anyone from calling certain functions from outside of the class. The outside user should never be able to know all the properties and methods of a class. They should only know the methods that are public
and needed for the usage of the class.
Let's improve our Shape
class by adding some abstractions to it. The end-user (inside the Main
class) can only call the methods that are needed to use a Shape
object.
1// Shape.js
2class Shape {
3 // To make a class thread-safe,
4 // It is recommended to make all properties of object private
5 // We will later inherit this class. protected is a keyword that allows inherited classes to access parent class properties.
6 maxWidth;
7 maxHeight;
8
9 getMaxWidth() {
10 return this.maxWidth;
11 }
12
13 getMaxHeight() {
14 return this.maxHeight;
15 }
16
17 setMaxWidth(maxWidth) {
18 this.maxWidth = maxWidth;
19 }
20
21 setMaxHeight(maxHeight) {
22 this.maxHeight = maxHeight;
23 }
24
25 getMaxArea() {
26 return this.maxWidth*this.maxHeight;
27 }
28}
29
30// Now another programmer will use the Shape's public methods
31
32const shape = new Shape();
33
34// Access public methods
35console.log(shape.getMaxHeight());
36console.log(shape.getMaxWidth());
37
38// Try to access private data -> Compilation error
39console.log(shape.maxWidth);
A method that returns a property of an object is known as a getter method. A method that sets a property value is known as a setter method. These are often used in Java to make properties private and accessible only through methods. You could also do some validation or calculation before returning a property if you are using a getter or setter. We will learn more about getters and setters in a later lesson.
Inheritance
The biggest draw of OOP
is its inheritance
property.

Inheritance
is the mechanism that binds two classes in a parent-child relationship. In this relationship, the child can access all the properties and methods of the parent. An object can originate from a more specific type of the parent class which allows you to inherit that object as a child from its parent class. If the behavior of the child class needs to change, we can override or add to the methods of the parent class to work as we want. We cannot change the function signature though.
In the Shape
class example, think of a Circle
or a Rectangle
. Both of these objects also need a getMaxArea
method, a maxWidth
property, and a maxHeight
property. Instead of reimplementing the same methods for these new classes by copy-pasting, we can just inherit these two classes from the Shape
class.
Let's implement it now! Java has a keyword extends
which applies inheritance to a class. See the example below.
1// Circle.java
2public class Circle extends Shape {
3 // Here, we don't need to define maxHeight and maxWidth. They are already defined
4 // Later we will learn how to initialize these when we learn about constructors
5
6 // Adding new properties
7 private int radius;
8
9 // Adding new methods
10 public int getRadius() { return radius; }
11 public void setRadius(int radius) { this.radius = radius; }
12 public double getArea() {
13 return 3.14*radius*radius;
14 }
15}
16
17// Rectangle.java
18public class Rectangle extends Shape {
19 private int height;
20 private int width;
21
22 // Adding new methods
23 public int getHeight() { return height; }
24 public int getWidth() { return width; }
25 public void setWidth(int width) { this.width = width; }
26 public void setHeight(int height) { this.height = height; }
27 public int getArea() {
28 return height*width;
29 }
30}
31
32// Main.java
33public class Main {
34 public static void main(String[] args) {
35 Circle circle = new Circle();
36 // Calling circle's own methods
37 System.out.println(circle.getArea());
38 // Calling parent classes methods
39 System.out.println(circle.getMaxArea());
40 }
41}
Note that, inheritance is only allowed if the two objects can be thought of as the same or similar. A circle is a shape. A rectangle is also a shape.
On the other hand, if we wanted to implement a Line
class, then it could not be inherited from Shape
since a line is not a shape. Also, it should not have methods like getArea
. However, a shape can contain a number of lines. We will discuss this more when we talk about Composition and Association in a later lesson.
Let's test your knowledge. Fill in the missing part by typing it in.
Which keyword in Java is used to inherit class A
from class B
?
Write the missing line below.
Polymorphism
The last of the four fundamental properties of OOP is polymorphism. Polymorphism
is a property that says all objects must act like all its parent types from which it is inherited. It can also act like other types (e.g., interfaces). In this section, we will discuss only the first part and leave interfaces for another lesson.

Let's take our Shape
class example and build a program with user inputs. In our console program, the user will select a number of different shapes and input the necessary properties of those shapes. Our program will then calculate the total area of all the shapes and output the sum!
The first thing we need for each shape is a method that takes values from the user. Below is the code for it. Notice that we are using an annotation @Override
to override a method. This helps the IDE to identify your mistake if you accidentally create a new method instead of overriding an existing method (e.g. change parameter order or type).
xxxxxxxxxx
}
// Shape.java
public class Shape {
protected int maxWidth;
protected int maxHeight;
// Getters and Setters
public int getMaxWidth() { return maxWidth; }
public int getMaxHeight() { return maxHeight; }
public void setMaxWidth(int maxWidth) { this.maxWidth = maxWidth; }
public void setMaxHeight(int maxHeight) { this.maxHeight = maxHeight; }
public int getMaxArea() { return maxWidth*maxHeight; }
public double getArea() {
return getMaxArea();
}
public void inputFromUser() {
Scanner sc= new Scanner(System.in);
System.out.print("Please input maxHeight :");
this.maxHeight = sc.nextInt();
System.out.print("Please input maxWidth :");
this.maxWidth = sc.nextInt();
System.out.println("Shape input successful");
}
}
// Circle.java
public class Circle extends Shape {
protected int radius;
The Wrong Way
Our classes are ready. Now we can write the main program. If we did not know about polymorphism, we would have had to do something like the program below.
xxxxxxxxxx
}
// This is not good practice
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter number of shapes to calculate : ");
int n = sc.nextInt();
ArrayList<Double> areas = new ArrayList<>();
for(int i = 0; i<n; i++) {
System.out.print("Enter the shape type : \n1) Rectangle\n2) Square\n3) Circle\nInput : ");
int select = sc.nextInt();
double area;
switch(select) {
case 1:
Rectangle rect = new Rectangle();
rect.inputFromUser();
area = rect.getArea();
areas.add(area);
break;
case 2:
Square square = new Square();
square.inputFromUser();
area = square.getArea();
areas.add(area);
break;
case 3:
Circle circle = new Circle();
circle.inputFromUser();
xxxxxxxxxx
})
// This is not good practice
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
})
console.log("Enter number of shapes to calculate : ")
readline.question(answer => {
const n = parseInt(answer)
const areas = []
for(let i = 0; i<n; i++) {
console.log("Enter the shape type : \n1) Rectangle\n2) Square\n3) Circle\nInput : ")
readline.question(shape => {
const select = parseInt(shape)
switch(select) {
case 1:
let rect = new Rectangle();
rect.inputFromUser();
let area = rect.getArea();
areas.push(area);
break;
case 2:
let square = new Square();
square.inputFromUser();
area = square.getArea();
The Polymorphism Way
The implementation above has several drawbacks. What if we wanted to do some other operations on the user's given shapes? We would have to do that operation on each object in each case
block. What if you had hundreds of different shapes? You would need to update hundreds of blocks just to add a similar-looking line! We would also need more ArrayLists
to hold the results.
Polymorphism to the rescue! Let's re-implement this program using the concept of polymorphism. We know that all rectangles, circles, and squares are a type of Shape, so we can declare all of these as shapes and use a common rule for them. We will do the following things in our code:
- Create a shapeClasses array where all the blueprints (class references like Rectangle, Square, etc.) will be kept.
- When a user gives inputs to create an ith shape, we will automatically create an instance of that class from the shapeClasses array.
- Note that we create the (i-1)th shape according to the user input
i
since arrays are 0 indexed.
- Note that we create the (i-1)th shape according to the user input
xxxxxxxxxx
}
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("Enter number of shapes to calculate : ");
int n = sc.nextInt();
// Create a list of classes
ArrayList<Class> shapeClasses = new ArrayList<>();
// Popular with our 3 shapes
shapeClasses.add(Rectangle.class); shapeClasses.add(Square.class); shapeClasses.add(Circle.class);
// A list of user created shapes
ArrayList<Shape> shapes = new ArrayList<>();
for (int i = 0; i < n; i++) {
System.out.print("Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : ");
int select = sc.nextInt();
// Input validation
if(select > 3 || select < 1) {
System.out.print("Please Enter a valid Value"); i--; continue;
}
try {
Shape shape = (Shape) shapeClasses.get(select-1).getDeclaredConstructor().newInstance();
shape.inputFromUser();
shapes.add(shape);
} catch (Exception e) {
e.printStackTrace();
}
}
// Now we calculate the average from the shapes list
Double average = shapes.stream().mapToDouble(val -> val.getArea()).average().orElse(0.0);
javascript
xxxxxxxxxx
class Main {
constructor() {
this.shapeClasses = [Rectangle, Square, Circle];
this.shapes = [];
}
shapesCalculation(n) {
for (let i = 0; i < n; i++) {
let select = prompt("Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : ");
if(select > 3 || select < 1) {
console.log("Please Enter a valid Value");
i--;
continue;
}
let shape = new this.shapeClasses[select-1]();
shape.inputFromUser();
this.shapes.push(shape);
}
let total = this.shapes.reduce((sum, shape) => sum + shape.getArea(), 0);
let average = total / this.shapes.length;
console.log("The average of all shapes' area is : " + average);
}
}
python
xxxxxxxxxx
class Main:
def __init__(self):
self.shapeClasses = [Rectangle, Square, Circle]
self.shapes = []
def shapesCalculation(self, n):
for i in range(n):
select = int(input("Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : "))
if select > 3 or select < 1:
print("Please Enter a valid value")
continue
shape = self.shapeClasses[select-1]()
shape.inputFromUser()
self.shapes.append(shape)
total = sum(shape.getArea() for shape in self.shapes)
average = total / len(self.shapes) if self.shapes else 0
print(f"The average of all shapes' area is : {average}")
cpp
xxxxxxxxxx
class Main {
public:
vector<Shape*> shapes;
vector<Shape*> shapeClasses = {new Rectangle(), new Square(), new Circle()};
void shapesCalculation(int n) {
for (int i = 0; i < n; i++) {
cout << "Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : ";
int select;
cin >> select;
if(select > 3 || select < 1) {
cout << "Please enter a valid value";
continue;
}
shapes.push_back(shapeClasses[select-1]);
shapes.back()->inputFromUser();
}
double total = 0.0;
for (Shape* shape : shapes) {
total += shape->getArea();
}
double average = (shapes.size() ? total/shapes.size() : 0);
cout << "The average of all shapes' area is : " << average << endl;
}
};
csharp
xxxxxxxxxx
class MainClass {
ArrayList shapeClasses = new ArrayList() { typeof(Rectangle), typeof(Square), typeof(Circle) };
ArrayList shapes = new ArrayList();
public void ShapesCalculation(int n) {
for (int i = 0; i < n; i++) {
Console.WriteLine("Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : ");
int select = Convert.ToInt32(Console.ReadLine());
if (select > 3 || select < 1) {
Console.WriteLine("Please Enter a valid value");
continue;
}
var shape = (Shape)Activator.CreateInstance((Type)shapeClasses[select-1]);
shape.InputFromUser();
shapes.Add(shape);
}
double total = 0;
foreach (Shape shape in shapes) {
total += shape.GetArea();
}
double average = (shapes.Count > 0 ? total/shapes.Count : 0);
Console.WriteLine("The average of all shapes' area is : " + average);
}
}
go
xxxxxxxxxx
type Main struct {
ShapeClasses []Shape
Shapes []Shape
}
func (m *Main) shapesCalculation(n int){
for i := 0; i < n; i++ {
fmt.Println("Enter the shape type\n1) Rectangle\n2) Square\n3) Circle\nInput : ")
var select int
fmt.Scanln(&select)
if select > 3 || select < 1 {
fmt.Println("Please enter a valid value")
continue
}
shape := m.ShapeClasses[select-1]
shape.inputFromUser()
m.Shapes = append(m.Shapes, shape)
}
var total float64
for _, shape := range m.Shapes {
total += shape.getArea()
}
var average float64
if len(m.Shapes) != 0 {
average = total / float64(len(m.Shapes))
}
fmt.Printf("The average of all shapes' area is : %f\n", average)
}
Some notes:
- Ignore the
getDeclaredConstructor
part for now. - We are instantiating an object from the class with the
newInstance
method instead of thenew
keyword. There are many more ways to instantiate an object. There are also several design patterns (e.g. Factory Design Pattern) to create objects. - Notice that we are putting all the subclasses of Shape (Rect, Circle, etc.) into the Shape type. This means that Rectangle, Circle, and Square act as Shape types which is an example of polymorphism.
- If a class in shapeClasses is not inherited from Shape, then the compiler will panic which is why we need a try-catch block.
Notice how much more concise the code becomes when compared to the previous one. Also, if we had to do some other stuff and call other methods, we can just add that command after the for
loop just like how we called the getArea()
method: writing it once and applying it to all shapes. This is the power of polymorphism. Later we will learn more about what polymorphism can do when we understand interfaces and adapters.
Build your intuition. Click the correct answer from the options.
What is the console output of the below code snippet?
1class A {
2 public void methodCall() {
3 System.out.println("Call from A");
4 }
5}
6
7class B extends A {
8 public void methodCall() {
9 System.out.println("Call from B");
10 }
11}
12
13public class Main {
14 public static void main(String[] args) {
15 A a = new B();
16 a.methodCall();
17 }
18}
Click the option that best answers the question.
- Call from A
- Call from B
- Call from A Call from B
- Error
Conclusion
Whoa! Today was a big day for you. You have covered all the fundamental concepts of OOP. Although the basics are now out of the way, there are still a ton of things you need to know. In our next lesson, we will cover more core aspects of OOP including constructors, destructors, virtual methods, and abstract classes.
One Pager Cheat Sheet
- This lesson covers the four fundamental properties of
Object-Oriented Programming
and explains howEncapsulation
,Abstraction
,Inheritance
, andPolymorphism
help create an OOP language such as Java, C++, Python, or JavaScript, and the conditions for it to become a Pure OOP Language. - Encapsulation dictates that all data related to an object should be
inside
the object, meaning that an object cannot access properties if they areoutside
itself. - Abstraction in OOP is utilizing special keywords to prevent anyone from calling certain functions from outside of the class and letting users of the class only use the methods that are
public
and needed to use the class. - Inheritance allows code to be reused by creating a parent-child relationship between two classes, where one class can access all the properties and methods of the other, enabling objects to be created from a more specific type of the parent class.
- Java's keyword
extends
allows programmers to create more specific classes by inheriting properties and methods from a parent class, while adding or overriding existing methods and properties as needed. - The last of the four fundamental properties of OOP is Polymorphism, which enables objects to act like their parent types, as well as other types (e.g. interfaces).
Polymorphism
allows us to avoid writing cumbersome code for our main program.- We can use the concept of
Polymorphism
to create an array of shapes, where each input from the user is an instance of the corresponding class from the array. - We can use polymorphism to more concisely apply a single command to multiple types of objects, such as the
getArea()
method, with thefor
loop in this example. - The code snippet demonstrates polymorphism, allowing an object of type
A
to refer to a different typeB
, and use the corresponding implementation of the same method when called. - You have learned the fundamentals of
Object-Oriented Programming (OOP)
, and in the next lesson, you will explore moreadvanced concepts
such as constructors, destructors, virtual methods, and abstract classes.