OOP Basics - Junior Level¶
What is OOP?¶
Object-Oriented Programming (OOP) is a programming paradigm that organizes code around objects — data structures that bundle related data (fields/properties) and behavior (methods) together.
Why OOP Exists¶
Before OOP, procedural programming organized code as sequences of instructions operating on separate data. As programs grew larger, this led to:
- Spaghetti code — functions scattered everywhere, hard to trace data flow
- Data corruption — any function could modify any data
- Code duplication — similar logic repeated for different data types
- Difficult maintenance — changing one thing broke many other things
OOP solves these problems by: 1. Grouping data with its behavior — no more scattered functions 2. Hiding internal state — preventing accidental corruption 3. Enabling code reuse — through inheritance and composition 4. Modeling real-world entities — making code intuitive
The 4 Pillars of OOP¶
1. Encapsulation¶
Bundling data and methods together, hiding internal details from the outside world.
[Object]
- private data (hidden)
- public methods (exposed interface)
- Internal state is protected from direct access
2. Abstraction¶
Showing only essential features while hiding complexity. Users interact with a simplified interface.
3. Inheritance¶
Creating new classes based on existing ones, reusing and extending behavior.
4. Polymorphism¶
Objects of different types responding to the same method call in their own way.
Classes, Objects, Structs¶
Java — Classes and Objects¶
public class Dog {
// Fields (instance variables)
private String name;
private int age;
private String breed;
// Constructor
public Dog(String name, int age, String breed) {
this.name = name; // 'this' refers to the current object
this.age = age;
this.breed = breed;
}
// Methods
public void bark() {
System.out.println(this.name + " says: Woof!");
}
public String describe() {
return this.name + " (" + this.breed + ", " + this.age + " years)";
}
// Getters and Setters (encapsulation)
public String getName() {
return this.name;
}
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
}
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age >= 0) {
this.age = age;
}
}
}
// Usage
public class Main {
public static void main(String[] args) {
Dog myDog = new Dog("Rex", 3, "German Shepherd");
myDog.bark(); // Rex says: Woof!
System.out.println(myDog.describe()); // Rex (German Shepherd, 3 years)
myDog.setAge(4);
// myDog.age = -5; // ERROR: age is private, can't access directly
}
}
Python — Classes and Objects¶
class Dog:
# Class variable (shared by all instances)
species = "Canis familiaris"
# Constructor (__init__ is the initializer)
def __init__(self, name: str, age: int, breed: str):
self.name = name # 'self' refers to the current instance
self.age = age
self._breed = breed # Convention: underscore = "private"
# Methods
def bark(self) -> None:
print(f"{self.name} says: Woof!")
def describe(self) -> str:
return f"{self.name} ({self._breed}, {self.age} years)"
# Property (Pythonic encapsulation)
@property
def breed(self) -> str:
return self._breed
@property
def age_in_human_years(self) -> int:
return self.age * 7
# Usage
my_dog = Dog("Rex", 3, "German Shepherd")
my_dog.bark() # Rex says: Woof!
print(my_dog.describe()) # Rex (German Shepherd, 3 years)
print(my_dog.age_in_human_years) # 21
print(my_dog.breed) # German Shepherd
Go — Structs and Methods¶
Go does not have classes. It uses structs (data) + methods (behavior attached to types).
package main
import "fmt"
// Struct definition (like a class without inheritance)
type Dog struct {
name string // lowercase = unexported (private)
age int
breed string
}
// Constructor function (Go convention: NewTypeName)
func NewDog(name string, age int, breed string) *Dog {
return &Dog{
name: name,
age: age,
breed: breed,
}
}
// Method with receiver (like 'this' or 'self')
func (d *Dog) Bark() { // Uppercase = exported (public)
fmt.Printf("%s says: Woof!\n", d.name)
}
func (d *Dog) Describe() string {
return fmt.Sprintf("%s (%s, %d years)", d.name, d.breed, d.age)
}
// Getter
func (d *Dog) Name() string {
return d.name
}
// Setter with validation
func (d *Dog) SetAge(age int) {
if age >= 0 {
d.age = age
}
}
func main() {
myDog := NewDog("Rex", 3, "German Shepherd")
myDog.Bark() // Rex says: Woof!
fmt.Println(myDog.Describe()) // Rex (German Shepherd, 3 years)
myDog.SetAge(4)
// myDog.age = -5 // Works within same package, but convention says use setter
}
Constructors / Initialization¶
Java Constructors¶
public class Person {
private String name;
private int age;
// Default constructor
public Person() {
this.name = "Unknown";
this.age = 0;
}
// Parameterized constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Constructor chaining
public Person(String name) {
this(name, 0); // calls the 2-arg constructor
}
}
// Usage
Person p1 = new Person(); // Default
Person p2 = new Person("Alice", 25); // Parameterized
Person p3 = new Person("Bob"); // Chained → age = 0
Python __init__¶
class Person:
def __init__(self, name: str = "Unknown", age: int = 0):
self.name = name
self.age = age
@classmethod
def from_string(cls, data: str) -> "Person":
"""Alternative constructor (factory method)."""
name, age = data.split(",")
return cls(name.strip(), int(age.strip()))
def __repr__(self) -> str:
return f"Person(name='{self.name}', age={self.age})"
# Usage
p1 = Person() # Default values
p2 = Person("Alice", 25) # Positional
p3 = Person(name="Bob") # Keyword → age = 0
p4 = Person.from_string("Eve, 30") # Factory method
Go Constructor Functions¶
type Person struct {
name string
age int
}
// Standard constructor
func NewPerson(name string, age int) *Person {
return &Person{name: name, age: age}
}
// Constructor with defaults
func NewDefaultPerson() *Person {
return &Person{name: "Unknown", age: 0}
}
// Functional options pattern (advanced but common in Go)
type PersonOption func(*Person)
func WithName(name string) PersonOption {
return func(p *Person) { p.name = name }
}
func WithAge(age int) PersonOption {
return func(p *Person) { p.age = age }
}
func NewPersonWithOptions(opts ...PersonOption) *Person {
p := &Person{name: "Unknown", age: 0}
for _, opt := range opts {
opt(p)
}
return p
}
// Usage
p1 := NewPerson("Alice", 25)
p2 := NewDefaultPerson()
p3 := NewPersonWithOptions(WithName("Bob"), WithAge(30))
Fields/Properties and Methods¶
Java¶
public class BankAccount {
// Fields
private String owner;
private double balance;
private static int accountCount = 0; // Class-level field (shared)
public BankAccount(String owner, double initialBalance) {
this.owner = owner;
this.balance = initialBalance;
accountCount++;
}
// Instance method
public void deposit(double amount) {
if (amount > 0) {
this.balance += amount;
}
}
public boolean withdraw(double amount) {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
return true;
}
return false;
}
// Static method (belongs to class, not instance)
public static int getAccountCount() {
return accountCount;
}
// Getter
public double getBalance() {
return this.balance;
}
}
Python¶
class BankAccount:
# Class variable
account_count = 0
def __init__(self, owner: str, initial_balance: float = 0.0):
self.owner = owner
self._balance = initial_balance # "private" by convention
BankAccount.account_count += 1
# Instance method
def deposit(self, amount: float) -> None:
if amount > 0:
self._balance += amount
def withdraw(self, amount: float) -> bool:
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
# Property (computed field)
@property
def balance(self) -> float:
return self._balance
# Static method (no access to instance or class)
@staticmethod
def validate_amount(amount: float) -> bool:
return amount > 0
# Class method (access to class, not instance)
@classmethod
def get_account_count(cls) -> int:
return cls.account_count
Go¶
type BankAccount struct {
owner string
balance float64
}
// accountCount is package-level (like static)
var accountCount int
func NewBankAccount(owner string, initialBalance float64) *BankAccount {
accountCount++
return &BankAccount{owner: owner, balance: initialBalance}
}
// Methods use pointer receivers to modify state
func (a *BankAccount) Deposit(amount float64) {
if amount > 0 {
a.balance += amount
}
}
func (a *BankAccount) Withdraw(amount float64) bool {
if amount > 0 && amount <= a.balance {
a.balance -= amount
return true
}
return false
}
// Value receiver (doesn't modify, just reads)
func (a BankAccount) Balance() float64 {
return a.balance
}
// Package-level function (like static method)
func GetAccountCount() int {
return accountCount
}
Access Modifiers¶
Java Access Modifiers¶
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | No |
| (default) | Yes | Yes | No | No |
private | Yes | No | No | No |
public class Example {
public String publicField; // Accessible everywhere
protected String protectedField; // Same package + subclasses
String packageField; // Same package only (default)
private String privateField; // This class only
}
Go — Exported vs Unexported¶
Go uses capitalization for visibility:
| Rule | Visibility |
|---|---|
Uppercase | Exported (public) |
lowercase | Unexported (private) |
Visibility is at the package level, not struct level.
package user
type User struct {
Name string // Exported: accessible from other packages
email string // Unexported: accessible only within 'user' package
}
func (u *User) GetEmail() string { // Exported method
return u.email
}
func (u *User) validate() bool { // Unexported method
return u.email != ""
}
Python — Naming Conventions¶
Python has no enforced access control — it uses naming conventions:
| Convention | Meaning |
|---|---|
name | Public |
_name | Protected (convention: "internal use") |
__name | Private (name mangling: _ClassName__name) |
__name__ | Dunder/magic method (special to Python) |
class Example:
def __init__(self):
self.public = "anyone can access"
self._protected = "please don't access from outside"
self.__private = "name-mangled to _Example__private"
e = Example()
print(e.public) # Works
print(e._protected) # Works but frowned upon
# print(e.__private) # AttributeError!
print(e._Example__private) # Works (name mangling), but never do this
this / self Keyword¶
Java — this¶
public class Point {
private int x, y;
public Point(int x, int y) {
this.x = x; // 'this' disambiguates field from parameter
this.y = y;
}
public double distanceTo(Point other) {
int dx = this.x - other.x;
int dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
// 'this' can be passed to other methods
public void register(Registry registry) {
registry.add(this);
}
}
Python — self¶
class Point:
def __init__(self, x: int, y: int):
self.x = x # 'self' is explicitly the first parameter
self.y = y
def distance_to(self, other: "Point") -> float:
dx = self.x - other.x
dy = self.y - other.y
return (dx ** 2 + dy ** 2) ** 0.5
def register(self, registry):
registry.add(self)
Go — Receiver Variable¶
type Point struct {
x, y int
}
// 'p' is the receiver — Go convention: 1-2 letter abbreviation of type
func (p *Point) DistanceTo(other *Point) float64 {
dx := float64(p.x - other.x)
dy := float64(p.y - other.y)
return math.Sqrt(dx*dx + dy*dy)
}
// Value receiver — doesn't modify the struct
func (p Point) String() string {
return fmt.Sprintf("(%d, %d)", p.x, p.y)
}
Complete Example: A Shape System¶
Java¶
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() {
return this.width * this.height;
}
public double perimeter() {
return 2 * (this.width + this.height);
}
public void scale(double factor) {
this.width *= factor;
this.height *= factor;
}
@Override
public String toString() {
return String.format("Rectangle(%.1f x %.1f)", width, height);
}
}
public class Circle {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
public double area() {
return Math.PI * this.radius * this.radius;
}
public double perimeter() {
return 2 * Math.PI * this.radius;
}
@Override
public String toString() {
return String.format("Circle(r=%.1f)", radius);
}
}
// Usage
public class Main {
public static void main(String[] args) {
Rectangle rect = new Rectangle(5, 3);
System.out.println(rect); // Rectangle(5.0 x 3.0)
System.out.println(rect.area()); // 15.0
System.out.println(rect.perimeter()); // 16.0
rect.scale(2);
System.out.println(rect.area()); // 60.0
Circle circle = new Circle(4);
System.out.println(circle); // Circle(r=4.0)
System.out.println(circle.area()); // 50.265...
}
}
Python¶
import math
class Rectangle:
def __init__(self, width: float, height: float):
self._width = width
self._height = height
def area(self) -> float:
return self._width * self._height
def perimeter(self) -> float:
return 2 * (self._width + self._height)
def scale(self, factor: float) -> None:
self._width *= factor
self._height *= factor
def __repr__(self) -> str:
return f"Rectangle({self._width:.1f} x {self._height:.1f})"
class Circle:
def __init__(self, radius: float):
self._radius = radius
def area(self) -> float:
return math.pi * self._radius ** 2
def perimeter(self) -> float:
return 2 * math.pi * self._radius
def __repr__(self) -> str:
return f"Circle(r={self._radius:.1f})"
# Usage
rect = Rectangle(5, 3)
print(rect) # Rectangle(5.0 x 3.0)
print(rect.area()) # 15.0
print(rect.perimeter()) # 16.0
rect.scale(2)
print(rect.area()) # 60.0
circle = Circle(4)
print(circle) # Circle(r=4.0)
print(circle.area()) # 50.265...
Go¶
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width float64
height float64
}
func NewRectangle(width, height float64) *Rectangle {
return &Rectangle{width: width, height: height}
}
func (r *Rectangle) Area() float64 {
return r.width * r.height
}
func (r *Rectangle) Perimeter() float64 {
return 2 * (r.width + r.height)
}
func (r *Rectangle) Scale(factor float64) {
r.width *= factor
r.height *= factor
}
func (r Rectangle) String() string {
return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.width, r.height)
}
type Circle struct {
radius float64
}
func NewCircle(radius float64) *Circle {
return &Circle{radius: radius}
}
func (c Circle) Area() float64 {
return math.Pi * c.radius * c.radius
}
func (c Circle) Perimeter() float64 {
return 2 * math.Pi * c.radius
}
func (c Circle) String() string {
return fmt.Sprintf("Circle(r=%.1f)", c.radius)
}
func main() {
rect := NewRectangle(5, 3)
fmt.Println(rect) // Rectangle(5.0 x 3.0)
fmt.Println(rect.Area()) // 15
fmt.Println(rect.Perimeter()) // 16
rect.Scale(2)
fmt.Println(rect.Area()) // 60
circle := NewCircle(4)
fmt.Println(circle) // Circle(r=4.0)
fmt.Println(circle.Area()) // 50.265...
}
Cheat Sheet: OOP Syntax Across Go, Java, Python¶
| Concept | Go | Java | Python |
|---|---|---|---|
| Define type | type Dog struct{} | class Dog {} | class Dog: |
| Create instance | d := Dog{} or NewDog() | Dog d = new Dog() | d = Dog() |
| Constructor | func NewDog() *Dog | public Dog() {} | def __init__(self): |
| Instance field | d.name (struct field) | this.name | self.name |
| Method | func (d *Dog) Bark() | public void bark() | def bark(self): |
| Self reference | d (receiver var) | this | self |
| Public | Name (uppercase) | public | name (no prefix) |
| Private | name (lowercase) | private | _name (convention) |
| Static method | Package function | static keyword | @staticmethod |
| Class method | N/A | N/A (use static) | @classmethod |
| ToString | func (d Dog) String() | public String toString() | def __repr__(self): |
| Equality | == (value types) | .equals() | def __eq__(self, other): |
| Inheritance | Embedding (composition) | extends | class Child(Parent): |
| Interface | Implicit (duck typing) | implements | ABC + @abstractmethod |
| Null/None | nil | null | None |
| Package/Module | package main | package com.example | Module = file |
Key Differences Summary¶
- Go has NO classes — uses structs + methods + interfaces
- Go has NO inheritance — uses composition (embedding)
- Go interfaces are implicit — no
implementskeyword needed - Python has NO true private — uses naming conventions only
- Java is the most "traditional" OOP — has all classical OOP features
- Go is the most minimal — intentionally omits many OOP features
Key Takeaways¶
- OOP bundles data and behavior together into objects
- The 4 pillars (encapsulation, abstraction, inheritance, polymorphism) guide OOP design
- Java uses classical OOP with classes, Python uses classes with conventions, Go uses structs with methods
- Access control varies: Java has keywords, Go uses capitalization, Python uses conventions
- Constructors initialize objects — each language has its own pattern
- Understanding OOP basics is the foundation for design patterns and architecture