Course program← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

The lifecycle of an object

This lesson was translated automatically – it may contain errors.

1 · Creation 2 · Use 3 · Destruction constructor runs, memory allocated fields change, methods work no references left, memory released

An object in a program has a beginning and an end: it is created, it works, and it disappears from memory. We will go through all three stages, and dwell longest on the first – that is where the object gets its state, and where it is easiest to spoil.

Once more, to be clear: the details of creating and removing objects differ from language to language. In some everything is automated by a garbage collector; in others part of the objects is released on its own and part is left to you – as in C++, where an object on the stack dies when it leaves its scope, while one created with new lives until it is deleted.

The three stages of a life

1. Creation

  • An object is created from a class by a special method, the constructor.
  • At that moment memory is allocated for the object and its attributes are given their initial values.
// A constructor is always named after its class
var myCar = new Car("red", "Tesla");
// Fields left without a value get a default: 0, false, null
// The constructor carries the class name here too
Car myCar("red", "Tesla");
// But a field left uninitialised holds garbage, not zero
// Go has no constructors: by convention a New… function plays that role
myCar := NewCar("red", "Tesla")
// Fields left without a value are always zeroed: 0, "", nil
# The constructor is the __init__ method
my_car = Car("red", "Tesla")
# An attribute does not exist until something is assigned to it
// The constructor is declared with the word constructor
const myCar = new Car("red", "Tesla");
// A field declared without a value equals undefined
// A constructor is always named after its class
Car myCar = new Car("red", "Tesla");
// Fields of an object get their defaults: 0, false, null
// The primary constructor is the class header, no new needed
val myCar = Car("red", "Tesla")
// A property must be given a value, otherwise the code will not compile

2. Use

  • The program interacts with the object: it reads the attributes and calls the methods.
  • The values of the attributes may change while the program runs.
myCar.StartEngine();    // a method changes the state of the object
myCar.Color = "green";  // a property is read and written like a field
Console.WriteLine(myCar.Color);
myCar.StartEngine();            // a method changes the state of the object
myCar.set_color("green");       // no properties here – a method changes the value
std::cout << myCar.color() << std::endl;
myCar.StartEngine()     // a method changes the state of the object
myCar.Color = "green"   // an exported field is available directly
fmt.Println(myCar.Color)
my_car.start_engine()   # a method changes the state of the object
my_car.color = "green"  # an attribute is read and written directly
print(my_car.color)
myCar.startEngine();    // a method changes the state of the object
myCar.color = "green";  // a field is read and written directly
console.log(myCar.color);
myCar.startEngine();        // a method changes the state of the object
myCar.setColor("green");    // fields are private – a setter changes the value
System.out.println(myCar.getColor());
myCar.startEngine()     // a method changes the state of the object
myCar.color = "green"   // the assignment goes through the property's setter
println(myCar.color)

3. Destruction

  • When the object is no longer needed, it is removed from memory. In some languages (C++, for instance) this happens by hand; in others (Python, Java, C#) a garbage collector takes care of it.

Destruction is far more involved than creation, and here we have only pointed at it. Collector generations, collection pauses, leaks through references that stay alive, releasing resources by hand – interviews devote a whole block of questions to this, and by volume it is the subject of a course of its own, not this one.

Next, the first stage in detail. An object appears in memory empty, and the job of the constructor is to make sure it is in a correct state from its very first second.

Why a constructor is needed

You can create an object and then set the values of its fields from outside. That is a bad idea, and not for the sake of beauty: between creation and filling, the object exists half-assembled. If somebody calls a method of it at that moment, the behaviour is unpredictable.

The constructor closes that gap: until it has finished, the object does not exist for the outside code. So it makes sense to put into it everything without which the object is meaningless.

The rule is simple: if the object does not work without a value, that value must arrive through the constructor. The rest can be set later.

Checks in the constructor

The constructor is the last place where you can still refuse to create the object. If the model of the car is empty, better to throw an error right away than to catch a null ten calls later at the other end of the program.

It is the same technique as the private fields from the previous lesson: narrow the set of places where an incorrect state can appear.

Several constructors

An object can often be created in more than one way: with a full set of data and with a minimal one. Then several constructors appear, and it is easy to make trouble here – to copy the checks into each of them.

Better to have one «main» constructor with all the checks and make the rest call it, filling in the default values.

Example

In C# overloads are chained with : this(...)Go has no constructors: New… functions play that role, and one calls anotherPython has no overloads – default parameter values do the same jobTypeScript has essentially no constructor overloads – optional parameters are used insteadIn Java overloads are chained with this(...)Kotlin has a primary constructor in the class header and secondary ones via constructor.

public class Car
{
    // The main constructor: all the checks live here
    public Car(string color, string model)
    {
        if (string.IsNullOrWhiteSpace(model))
            throw new ArgumentException("The model is required", nameof(model));

        Color = color;
        Model = model;
    }

    // The overload does not duplicate the checks, it delegates to the main one
    public Car(string model) : this("white", model) { }

    public string Color { get; set; }
    public string Model { get; }
}
#include <stdexcept>

class Car {
public:
    // The main constructor: all the checks live here
    Car(std::string color, std::string model)
        : color_(std::move(color)), model_(std::move(model)) {
        if (model_.empty()) {
            throw std::invalid_argument("The model is required");
        }
    }

    // The delegating constructor does not duplicate the checks
    explicit Car(std::string model) : Car("white", std::move(model)) {}

    const std::string& color() const { return color_; }
    const std::string& model() const { return model_; }

private:
    std::string color_;
    std::string model_;
};
type Car struct {
	Color string
	Model string
}

// The main «constructor» is an ordinary function. We return an error, not panic.
func NewCar(color, model string) (*Car, error) {
	if strings.TrimSpace(model) == "" {
		return nil, errors.New("the model is required")
	}

	return &Car{Color: color, Model: model}, nil
}

// The variant with a default delegates to the main one
func NewWhiteCar(model string) (*Car, error) {
	return NewCar("white", model)
}
class Car:
    # Python has no overloads: a default value solves the same problem
    def __init__(self, model, color="white"):
        if not model or not model.strip():
            raise ValueError("The model is required")

        self.color = color
        self.model = model
class Car {
    // An optional parameter instead of a second overload
    constructor(
        public readonly model: string,
        public color: string = "white",
    ) {
        if (!model.trim()) {
            throw new Error("The model is required");
        }
    }
}
public class Car {
    private String color;
    private final String model;

    // The main constructor: all the checks are here
    public Car(String color, String model) {
        if (model == null || model.isBlank()) {
            throw new IllegalArgumentException("The model is required");
        }

        this.color = color;
        this.model = model;
    }

    // The overload delegates to the main one through this(...)
    public Car(String model) {
        this("white", model);
    }
}
class Car(val model: String, var color: String = "white") {
    // init runs right after the primary constructor
    init {
        require(model.isNotBlank()) { "The model is required" }
    }
}

The order of initialisation

Keep in mind that until the constructor has finished, the object is not ready yet. Calling methods from it that a subclass may override is a well-known source of bugs: the subclass gets control before its own fields have been filled in.

In summary: a constructor is responsible not for «filling in the fields» but for making sure an incorrect object never exists in the first place. Collect the checks in one constructor and let the others call it.