AlgoDaily

Special Methods and Concepts in Object Oriented Programming

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Special Methods in Object-Oriented Programming

Special Methods in Object-Oriented Programming

Now you know the bare bones of Object-Oriented Programming. But, as I said in the previous lesson, there is still much more you have to discover. In today's lesson, we will go through a number of special functions that almost all OOP-supported programming languages have for convenience.

Intro Image

Keep in mind that these functions are utility functions and are not essential for OOP programming languages, meaning that you can do the same things with or without these functions. However, you'll save a tremendous amount of effort and lines of code if you know these functions. So let's begin. We will be improving our previous example based on Shapes with the new knowledge we will learn in this lesson.

Let's recap what we learned previously and see the final implementation of all the Shape classes.

// 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();
    }
}

// Circle.java
public class Circle extends Shape {
    protected int radius;

    // Getters and Setters
    public int getRadius() { return radius; }
    public void setRadius(int radius) { this.radius = radius; }

    @Override
    public double getArea() {
        return 3.14*radius*radius;
    }
}


// Rectangle.java
public class Rectangle extends Shape {
    protected int height;
    protected int width;

    // Getters and Setters
    public int getHeight() { return height; }
    public int getWidth() { return width; }
    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }

    @Override
    public double getArea() {
        return (double)height*width;
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        // Your code here
    }
}

Constructors

Constructors

Constructors

Programmers can't imagine OOP without constructor and destructor functions. Constructor is a special function that helps you instantiate an object of a class. Whenever you instantiate an object, a default constructor method is called.

Let's say we want to create a Rectangle object with a specific width and height (5x6). You would probably do it like below:

// Main.java
public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle();
        rect.setHeight(5);
        rect.setHeight(6);
    }
}

Now, let's see what is actually happening behind the scenes.

Create all the attributes in memory with default values (0 for `height` and `width`).
Create a class reference in memory that has all the attributes.
Call 2 setter functions to reassign the attributes.

We can see that the attributes are first set to the default (0) when creating the instances. They are then overwritten with setter methods. Imagine you have a class with 10 attributes and you need to set all the attributes to create a usable object. In that case, you would need to call 10 setters which will increase the size of the code and make the code harder to read. This is where a constructor can help. Constructor is a function that is called during instantiation to initialize the attributes of the new object along with any other things you need to do.

Here is the same code to create a 5x6 rectangle but with only 1 line of constructor call.

// Main.java
public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle(5,6);
    }
}

Of course, we also need to define this constructor method which takes 2 values when creating from the class:

// Rectangle.java
public class Rectangle extends Shape {
    protected int height;
    protected int width;

    // Default Constructor
    public Rectangle() {   }

    // User specified constructor
    public Rectangle(int height, int width) {
        this.height = height;
        this.width = width;
    }

    // Rest of the methods and Attributes
}

Constructors are very special because they have syntax which differs from typical methods in some languages. Constructors do not return a value (not even null). You may notice that they don't even have a return type (not even void).

Constructors are really powerful for several reasons. Suppose each object needs to dynamically create an array of variable size in it as an attribute. The best way to allocate the memory for that array in the heap would be through constructors. Or if you want to instantiate other objects inside that class, you need to do that inside the constructor.

// A random class
public class MyClass {
    protected MyAnotherClass myProperty;
    protected int[] myArray;

    public MyClass(int propValue, int arraySize) {
        myProperty = new MyAnotherClass(propValue);
        myArray = new int[arraySize];
    }
}

There are various constructors depending on their use cases. Let's look at some of them in more detail.

Default Constructor

Default Constructor

Whenever you instantiate an object, you are calling a method (notice the double parentheses after new ClassName). This method is the default constructor. Default constructors do not take any parameters and do not need to be explicitly defined by the user. However, you can override the default constructor if you want. For example, if we want a rectangle to be 1x1 initially, then we will need to override the default constructor.

// Rectangle.java
public class Rectangle extends Shape {
    protected int height;
    protected int width;

    // Overriding default constructor
    public Rectangle() {
        this.height = 1;
        this.width = 1;
    }

    // Custom Constructor
    public Rectangle(int height, int width) {
        this.height = height;
        this.width = width;
    }

    // Rest of the methods and Attributes
}

Copy Constructor

Copy Constructor

Copy constructors are called when you want to create a new object from another object that is the same type. In a constructor, you would pass another previously created object with the same type. For example, if we want to create a new rectangle from an old rectangle, we write the copy constructor.

// Rectangle.java
public class Rectangle extends Shape {
    protected int height;
    protected int width;

    // Overriding default constructor
    public Rectangle() {
        this.height = 1;
        this.width = 1;
    }

    // Copy constructor
    public Rectangle(Rectangle other) {
        this.height = other.getHeight();
        this.width = other.getWidth();
    }

    // Custom Constructor
    public Rectangle(int height, int width) {
        this.height = height;
        this.width = width;
    }


    // Rest of the methods and Attributes
}

quiz 1

Which of these constructors is NOT provided by default for the below class?

public class A {
    public int aInt;
}

Shallow Copy vs Deep Copy

Shallow Copy vs Deep Copy

If you know the basics of pointers, then you may have heard of the shallow copy and deep copy. All classes have a default copy constructor which is a shallow copy constructor.

Shallow vs Deep

Shallow copying is copying by reference (as opposed to copying by value). You will not notice any difference if your class has only primitive types (int, float, double, etc.). They will be copied by value in shallow copy as well. Suppose, however, you have an array as an attribute in the class. If you copy the object using the default copy constructor, then only the reference (or pointer) of the array will be copied. As a result, if you change a value at an index in the first object, then the value in the second object's array at that index will also be changed. See the code below for an example of a shallow copy.

// Shallow Copy
// MyClass.java
public class MyClass {
    protected int[] value;

    // default constructor
    public MyClass() {
        value = {1,2,3,4,5}
    }

    // No Copy constructor, so the default will be used
}

// Main.java
public class Main {
    // Instantiating
    MyClass myObj1 = new MyClass();
    // Copying, will be using default constructor
    MyClass myObj2 = new MyClass(myObj1);

    myObj1.value[2] = 10;

    // This will print: 1 2 10 4 5
    for(int i=0; i<myObj1.value.length; i++) {
        System.out.println(myObj1.value[i]);
    }

    // But this will also print: 1 2 10 4 5
    for(int i=0; i<myObj2.value.length; i++) {
        System.out.println(myObj2.value[i]);
    }
}

Now, to define a deep copy constructor, we need to create a separate array and then copy each element to the new array. See the code below:

// Deep Copy
// MyClass.java
public class MyClass {
    // For the sake of this example, we make it public to avoid excess getters
    public int[] value;

    // default constructor
    public MyClass() {
        value = {1,2,3,4,5};
    }

    // Copy constructor (Deep Copy)
    public MyClass(MyClass other) {
        this.value = new int[5];
        for(int i = 0; i<5; i++) {
            this.value[i] = other.value[i];
        }
    }
}

// Main.java
public class Main {
    // Instantiating
    MyClass myObj1 = new MyClass();
    // Copying, will be using default constructor
    MyClass myObj2 = new MyClass(myObj1);

    myObj1.value[2] = 10;

    // This will print: 1 2 10 4 5
    for(int i=0; i<myObj1.value.length; i++) {
        System.out.println(myObj1.value[i]);
    }

    // This will now print unchanged: 1 2 3 4 5
    for(int i=0; i<myObj2.value.length; i++) {
        System.out.println(myObj2.value[i]);
    }
}

quiz 2

Which of the following classes needs a copy constructor for deep-copy implementation?

class A {
    int x;
}
class B {
    int x;
    float y;
}
class C {
    String x;
}
class D {
    Integer x;
}

Destructors

Destructors

Destructors are the exact opposite of constructor methods. They are automatically called when an object goes out of scope or when you forcefully delete an object.

It is difficult for a programmer to forcefully delete an object using a language with a garbage collector. Since Java is a garbage-collected language, you cannot predict when (or even if) an object will be destroyed. Hence, there is no direct equivalent of a destructor in Java.

According to a popular question on stackoverflow.

There is an inherited method called finalize, but this is called entirely at the discretion of the garbage collector. So for classes that need to explicitly tidy up, the convention is to define a close method and use finalize only for sanity checking (i.e. if close has not been called do it now and log an error).

The reason for this is that all Java objects are heap-allocated and garbage collected. Without explicit deallocation (i.e. C++'s delete operator), there is no sensible way to implement real destructors. You will have a chance to peek at an example of an OOP language with a destructor in C++. See the code below for the previously implemented MyClass class:

class MyClass {
public:
    int* array;

    // default constructor
    MyClass() {
        this->value = new int[5];
        for(int i = 1; i <= 5; i++) {
            this->value[i] = i;
        }
    }

    // Destructor
    ~MyClass() {
        delete [] this->array;
    }
}

Closing Resources

Closing Resources

In Java, destructors are not used for normal cleanup because the garbage collector decides when memory is reclaimed. However, some objects hold resources that should be released immediately, such as files, sockets, database connections, or streams.

For these cases, Java code usually defines a close() method. A common way to formalize this is by implementing the AutoCloseable interface. Then the object can be used inside a try-with-resources block, and Java will call close() automatically when the block ends.

So the Java-style replacement for a destructor is usually not "wait for the object to die". It is:

Open resource.
Use resource.
Close resource explicitly when done.

Here is a small example.

Closing Resources

```java
// DrawingFile.java
public class DrawingFile implements AutoCloseable {
    private String fileName;

    public DrawingFile(String fileName) {
        this.fileName = fileName;
        System.out.println("Opening " + fileName);
    }

    public void draw(Rectangle rect) {
        System.out.println("Drawing rectangle to " + fileName);
    }

    @Override
    public void close() {
        System.out.println("Closing " + fileName);
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        try (DrawingFile file = new DrawingFile("shapes.txt")) {
            Rectangle rect = new Rectangle(5, 6);
            file.draw(rect);
        }
        // close() is called automatically here
    }
}
```

quiz 2b

In Java, what is the best way to clean up a file, socket, or database connection when you are done using it?

Getter and Setter Functions

Getter and Setter Functions

We have already covered this a little in our previous lesson. In this lesson, we will be explaining the usage of getter and setter functions in more detail.

Getter Setter

Suppose that you are using the Shape class and all its subclasses to create a very large drawing project with many other collaborators. Many developers will be importing your drawables package (suppose this contains all your classes) and instantiating many objects. Among them, one person mistakenly puts this line of code somewhere in their large file.

Rectangle rect = new Rectangle(5,6);
// Do some stuff
rect.height = -1
// Try to do something like draw the rectangle on the screen

Later in his own implementations, he finds very weird bugs that should not be there. He is not even opening the file where he put the above line. Imagine the horrific nights he and others might have just because your class does not have any way to validate its attributes.

Somehow, you should check if the assigned value to the property is valid or not. The best way is to validate a value is to raise an Exception when a user attempts to set an invalid value. However, we will be covering exceptions in our next lesson. For now, we will just print to the console that the user is attempting to set an invalid value. Unfortunately, we cannot validate the value if it is directly assigned to the attribute which is why we need setter methods. When you use a setter, you can make sure that you are getting the correct value for the correct attribute.

There are other reasons you should use setter methods. For example, you should use setter methods if you always want to convert the given value to something else before assigning it to a variable or if you want to do some database/internet operations while setting attributes.

// Rectangle.java
public class Rectangle extends Shape {
    // Make these attributes protected/private so others will not use it outside
    protected int height;
    protected int width;

    // getters and setters will always be public
    // getters 
    public int getWidth() { return width; }
    public int getHeight() { return height; }

    // setters
    public void setWidth(int width) {
        if(width <= 0) {
            System.out.println("Invalid width for Rectangle");
            return;
        }
        this.width = width;
    }
    public void setHeight(int height) {
        if(height <= 0) {
            System.out.println("Invalid height for Rectangle");
            return;
        }
        this.height = height;
    }
    // Other methods and attributes
}

quiz 3

What should the proper java-beans convention-wise getter function name be for the property algodailyIsTheBest?

Static Methods

Class Methods/Static Methods

Suppose you need a value that will always be the same (but not fixed) for all objects of that class. If you had to do it with the knowledge you gathered on OOP till now, you might try to keep track of all the objects in an array and keep a property constant for all the objects when one object changes the value.

However, this approach is very tedious and not efficient at all. Most of the OOP-supported languages will give you the convenience of a static keyword. A static property or method is "static" to all the objects. Only one unit of memory will be allocated for a class so that it can be accessed from the class and all of its instances. All the objects of that class will be sharing this single value.

There are a lot of usages for static attributes and methods of a class. For example, we can keep a count of the number of instances created from that class by maintaining a static count attribute. This cannot be done with a normal attribute since it would be reset when a new object is created, plus all instances would have their own value for each attribute (which is not desired).

// ObjectCounter.java
public class ObjectCounter {
    private static int count = 0;

    // Note: we cannot access static attributes with this keyword
    public ObjectCounter() { count++; }

    // all methods using static attribute or method will need to be static
    public static int getCount() {
        return count;
    }

    public void finalize() { count--; }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        // Static attributes/methods can be accessed from the class
        System.out.println(ObjectCounter.getCount()); // 0

        ObjectCounter obj = new ObjectCounter();
        ObjectCounter obj2 = new ObjectCounter();
        ObjectCounter obj3 = new ObjectCounter();

        System.out.println(ObjectCounter.getCount()); // 3

        // This is the way to cleanly delete an object before scope in java
        obj.finalize(); obj = null;
        obj2.finalize(); obj2 = null;

        System.out.println(ObjectCounter.getCount()); // 2
    }
}

quiz 4

When should you consider a method of a class static?

Equality Comparison function

Equality Comparison function

Suppose you want to compare two types of the Shape class. Since Shapes are custom classes, the compiler does not know how to compare two objects with different properties or which property to take into account. First, we need to decide what we need to compare in order to determine the equality of two Shape objects.

Let's assert that we declare two shapes equal whenever their areas are equal. We can do this by overriding the default equals method.

public class Shape {
    protected int maxWidth;
    protected int maxHeight;

    public double getArea() {
        return getMaxArea();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null)
            return false;
        if (obj == this)
            return true;
        System.out.println("Comparing");
        return this.getArea() == ((Shape) obj).getArea();
    }
    // Other attributes and Methods
}

Class-Wide vs Object Logic

Before moving on, separate the two ideas you just saw:

  • static logic belongs to the class as a whole. Every instance sees the same shared value or behavior.
  • equals logic belongs to a comparison between objects. You decide which fields or derived values make two instances "the same."

In interviews, this distinction matters: use static for shared counters/utilities, and override equals when the default object identity check is not the equality rule your design needs.

Class-Wide vs Object Logic

Static vs Instance Logic

Before moving on, separate two ideas that often get mixed together:

  • static belongs to the class itself: one shared value or behavior, like a single ObjectCounter.count.
  • equals belongs to the object comparison rule: it tells Java what “same” means for your custom type.

In the visual, notice where the data lives and who answers the question: the class answers shared-state questions, while each object participates in equality checks.

Static vs Instance Logic

Stringify Function

Stringify Function

If you want to print your custom class to the console or make it a string to send to another object for some purpose, you'll most likely use a built-in class function named toString() in Java. Currently, the default toString() method returns the name of the class along with its package name as well as the hash code in hex format of the class calculated with another built-in method hashCode(). So if you were to print an object of type Rectangle, you would get something like this "shapes.Rectangle@abcdefgh" as output.

If you want to print something different, then - like other methods - you can override the toString() method and return whatever you want to print. For our example, we will print the name of the Rectangle with its package name and its area inside parentheses. All the built-in classes (Integer, Double, BigInteger, etc.) have overridden the toString() method to return a more informative string (the value instead of Class name and hash code).

package shapes;

// Rectangle.java
public class Rectangle extends Shape {
    protected int height;
    protected int width;

    @Override
    public double getArea() {
        return (double)height*width;
    }

    @Override
    public String toString() {
        return "shapes.Rectangle(area=" + this.getArea() + ")";
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Rectangle rect = new Rectangle();
        rect.setHeight(5); rect.setWidth(4);
        System.out.println(rect); // shapes.Rectangle(20)
    }
}

Conclusion

Conclusion

All of the above functions are very important to understand in order to get a good grasp of OOP. In the next lesson, instead of looking at implementations, we will go through some advanced concepts of OOP. After the next lesson, you will understand the full potential of OOP.

One Pager Cheat Sheet

One Pager Cheat Sheet

  • Object-Oriented Programming (OOP) provides special functions to help save time and effort when coding, such as getters and setters for classes, as demonstrated in the example of Shape and its subclasses.
  • Constructors are special-purpose methods used to set initial values and perform initialization tasks during object instantiation.
  • A default constructor is a method (instantiated with new ClassName()) that initiates an object, which can be overridden by the user if desired.
  • The Copy Constructor is used to create a new object from an existing object of the same type by passing the existing object as an argument.
  • A user-defined constructor takes one int argument and assigns it to the data member aInt in the class A.
  • **Shallow copy** copies by reference and copies the **pointer** of an array, while **deep copy** copies the data by making a *new array* for it.
  • A custom copy constructor for deep copy needs to be implemented for classes A, B and D in order to copy the primitive wrapper type and non-primitive type objects by value instead of by reference.
  • Java objects are heap-allocated and automatically garbage collected, so there is no direct equivalent of a destructor as found in languages such as C++, where explicit deallocation is used via the delete operator.
  • Usinggetter and setterfunctions ensures that values assigned to object attributes are valid and can help prevent errors from occurring when working in collaboration with other developers.
  • The correct getter function name for the algodailyIsTheBest property according to the JavaBeans convention is getAlgodailyIsTheBest.
  • Static attributes and methods provide a convenient way to share values among all instances of a class, and can be used to keep track of the number of objects created from that class.
  • A static method can be accessed from the class directly and can be useful for situations where all the objects of the class need to share state, such as keeping a count of all objects created of the same class.
  • We can compare different Shape objects by overriding the default equals method to determine equality based on their areas.
  • - Static answers "what is shared by the class?"; equals answers "when are two objects considered the same?"
  • - Static members are class-level; equals defines object-level comparison semantics.
  • Overriding the toString() method allows you to return a more informative string than the default output of the class name and hash code.
  • After completing the next lesson, you will have a full understanding of the Object-Oriented Programming capabilities.
Open interactive view →