Chapter 2

Object-Oriented Programming

The best programming paradigm, “objectively” speaking!

Subsections of Object-Oriented Programming

Introduction

Content Note

Much of the content in this chapter was adapted from Nathan Bean’s CIS 400 course at K-State, with the author’s permission. That content is licensed under a Creative Commons BY-NC-SA license.

A signature aspect of object-oriented languages is (as you might expect from the name), the existence of objects within the language. In this chapter, we take a deep look at objects, exploring why they were created, what they are at both a theoretical and practical level, and how they are used.

Key Terms

Some key terms to learn in this chapter are:

  • Encapsulation
  • State
  • Class
  • Object
  • Field
  • Attribute
  • Method
  • Property
  • Public
  • Private
  • Static

To begin, we’ll examine the term encapsulation.

Encapsulation

YouTube Video

Video Materials

The first criteria that Alan Kay set for an object-oriented language was encapsulation. In computer science, the term encapsulation refers to organizing code into units, which provide two primary benefits:

  • Providing a mechanism for organizing complex software
  • The ability to control access to encapsulated data and functionality

Think back to the FORTRAN EPIC model we introduced in an earlier module. All of the variables in that program were declared globally, and there were thousands of them. If we open the code today, could we even find where a variable was declared? Initialized? Used? Could we be sure that we found all the spots it was used?

Also, how easily could we determine what part of the system a particular block of code belonged to? If we knew the program involved modeling hydrology (how water moves through the soils), weather, erosion, plant growth, plant residue decomposition, soil chemistry, planting, harvesting, and chemical applications, could we find the code for each of those processes?

Recall from our discussion on the growth of computing that, as computers grew more powerful, we looked to use them in more powerful ways. The EPIC project grew from that desire - if we could model all the aspects influencing how well a crop grows, then we could use that to make better decisions in agriculture. Likewise, if we could model the processes involved in weather, we could help save lives by predicting dangerous storms! A century ago, the only way to know a tornado was coming when you heard its roaring winds approaching your home. Now we have warnings that conditions are favorable to produce one hours in advance! This is all thanks to our ability to use computers to model some very complex systems.

How do we go about writing those complex systems? We probably wouldn’t want to follow the model that the EPIC software gives us. And, thankfully, neither did most software developers at the time - so computer scientists set out to define better ways to write programs. David Parnas formalized some of the best ideas emerging from those efforts in his 1972 paper “On the Criteria To Be Used in Decomposing Systems into Modules”. 1

A data structure, its internal linkings, accessing procedures and modifying procedures are part of a single module.

Here he suggests organizing code into modules that group related variables and the procedures that operate upon them. For the EPIC module, this might mean all the code related to weather modeling would be moved into its own module. That means that if we needed to understand how weather was being modeled, we only had to look at the weather module.

They are not shared by many modules as is conventionally done.

Here he is laying the foundations for the concept we now call scope - the concept that a particular symbol (a variable or function name) is accessible only in certain locations within a program’s code. By limiting access to variables to the scope of a particular module, only code in that module can change the value. That way, we can’t accidentally change a variable declared in the weather module from somewhere else, like the soil chemistry module. This would be a very hard error to find, because if the weather module doesn’t seem to be working, that’s where we would probably spend our time looking for the error.

Programmers of the time referred to this practice as information hiding, as we “hid” parts of the program from other parts of the program. Parnas and his peers pushed for not just hiding the data, but also how the data was manipulated. By hiding these implementation details, they could prevent programmers who were used to the globally accessible variables of early programming languages from looking into our code and using a variable that we might change in the future.

The sequence of instructions necessary to call a given routine and the routine itself are part of the same module.

As the actual implementation of the code is hidden from other parts of the program, a mechanism for sharing controlled access to some part of that module in order to use it needed to be made. An interface, for example, that describes how the other parts of the program might trigger some behavior or access some value.


  1. D. L. Parnas, “On the criteria to be used in decomposing systems into modules” Communications of the ACM, Dec. 1972. ↩︎

Subsections of Encapsulation

Packages

Let’s start by focusing on encapsulation’s benefits to organizing our code by exploring some examples of encapsulation you may already be familiar with.

Packages

The Java and Python libraries are organized into discrete units called packages. The primary purpose of this is to separate code units that potentially use the same name, which causes name collisions where the compiler or interpreter isn’t sure which of the possibilities you mean in your program. This means you can use the same name to refer to two different things in your program, provided they are in different packages. Many other languages refer to these as namespaces.

For example, there are two definitions for a Date class in Java: java.util.Date and java.sql.Date. While they are related, they serve different purposes, and we wouldn’t want to get them confused. If we needed to create an instance of both in our program, we would use their fully-quantified name to help the compiler know which we mean:

Java
java.sql.Date sqlDate = new java.sql.Date(System.currentTimeMillis());
java.util.Date utilDate = new java.util.Date(System.currentTimeMillis());
System.out.println(sqlDate.toString());
System.out.println(utilDate.toString());

Running that code gives this output:

2020-12-30
Wed Dec 30 17:23:50 GMT 2020

So, as we can see, these two classes are functionally different in some important ways.

While Java does not support aliases in imports, we can use an alias in Python to import two classes with the same name using different identifiers. For example, if there are two User classes in different packages, we could import them like this:

Python
from package_one import User as PackageOneUser
from package_two import User as PackageTwoUser

user_1 = PackageOneUser.User()
user_2 = PackageTwoUser.User()

Encapsulating code within a package helps ensure that the types defined within are only accessible with a fully qualified name, or when the using directive is employed. In either case, the intended type is clear, and knowing the package can help other programmers find the type’s definition.

Creating Packages

We can also declare our own packages, allowing us to use packages to organize our own code just as Java and Python have done with their standard libraries. Below are quick examples for how to do this in Java and Python.

Java

To create a class ClassName in a package cc410.package_name, we would include a package line at the top of the file:

package cc410.package_name;

public class ClassName{
    //code here
}

The ClassName.java file would be stored in app/src/main/java/cc410/package_name/. Basically, the package name corresponds to the folders where the source code is stored.

Python

To create a class ClassName in a package cc410.package_name, we would simply place ClassName.py in the src/cc410/package_name directory. We’d also need to include an __init__.py file in that directory to make it a package.

Finally, if we want the cc410 package to act as a meta-package and be executable we would also include an __init__.py and a __main__.py file in the src/cc410 directory as well.

Seeing Double?

In previous textbooks, we created different sections for both Java and Python code, so generally students would only see one or the other.

In this class, we feel that it is important for developers to become familiar with more than one language, as it may help increase understanding. So, nearly all examples in this book will be presented using both Java and Python. We will clearly label each language where needed, but hopefully at this point you are comfortable enough with your chosen language to recognize it clearly.

Type Systems

YouTube Video

Video Materials

Before we go further into some object-oriented concepts, let’s briefly review one important concept in programming - data types and type systems.

Primitive Data Types

Most programming languages include several primitive data types, which are the fundamental units of data that can be stored and represented by that programming language. Here’s a short list of those primitive data types for each language:

Data Java Python
Whole Numbers int (byte, short, long) int
Floating-point Numbers double (float) float
Boolean Values boolean bool
Single Character char str^[A string of length 1]
String of Characters String^[This is not a primitive, but the String class. However, it is so ubiquitous that we’ll include it here.] str

Any data that is stored by our program must fit into one of these data types. That is an important fundamental rule to remember - no matter how complex our code gets, everything is stored in primitive data types. That’s simply all there is.

Complex Data

What if we want to store more complex data, such as information about a person? Well, we could easily create an integer that stores the person’s age, and perhaps a string for the person’s name. Those are still just primitive data types, so we’re good there.

However, as you probably already know, we can group those items together into classes. However, before we can really understand classes and how they relate to encapsulation, we must look at a precursor to classes first. We’ll cover that later in this module.

Type Systems

The way that programming languages handle these data types is known as the type system of the language. Let’s look at two different ways to categorize type systems to see how they differ.

Static Typing vs. Dynamic Typing

In programming, there are two common ways that programming languages deal with data types. The first is called static typing, where each variable has a particular data type associated with it as soon as it is declared, and that variable can only store items of that data type. Because of this, we can use tools like the Java compiler to analyze our code before we ever execute it, making sure that we always are storing the correct type of data in each variable.

Java is a statically typed language. When we create variables in Java, we must assign data types to them, as in this example:

Java
int x = 5;
double y = 5.5;
String name = "CC 410";

Similarly, when we create methods in Java, we must declare the types of all parameters, as well as the return type of the method.

Python, on the other hand, is a dynamically typed language. That means that variables in Python do not have a particular data type assigned to them, and they can store multiple different types of data throughout the course of the program. Here’s an example:

Python
x = 5
x = 5.5
x = "CC 410"

This is a perfectly valid program in Python, and will execute just fine. However, as we’ll soon learn, this could lead to some preventable errors, and we’ll see how to resolve them.

Strong Typing vs. Weak Typing

Programming languages can also be classified based on their use of type systems in one other way. A strongly typed language always knows what data type is stored in a variable at any given time during the program’s execution. In statically typed languages such as Java, this is trivial - if the program compiles, then we know that the only possible data type that could be stored in a variable is the type listed in that variable’s declaration. It’s pretty straightforward.

However, what about Python? Python is dynamically typed, which means that each variable could store multiple different data types during a single program’s execution, and each time the program executes it could be different. However, at any given instant during the execution of the program, the Python interpreter knows exactly what type of data is being stored in each of the variables in the program. We can use methods such as isinstance() to confirm this. So, Python is also a strongly typed language.

So, what is a weakly typed language? A great example is code written in an assembly language. The computer will simply execute whatever is written, and has no way of keeping track of the types of data stored in each variable. Instead, it depends on the compiler or developer to make sure there are no type errors in the assembly code.

Making Python Statically Typed

As we learned in the “Hello Real World” project, we can add type annotations to Python code to convert Python into a statically typed language. Then, we can use tools such as Mypy to make sure there are no type errors in our code, much like the Java compiler does for Java code. So, here’s a rewritten example of Python code that is statically typed:

Python
x: int = 5
y: float = 5.5
name: str = "CC 410"

By adding these type annotations, we can tell Mypy what type of data we expect to be stored in each of these variables, and it can perform the same type checking process that the Java compiler uses. In this class, we’re going to focus on using statically typed Python code as much as we can.

Why This Matters

We’re spending a little time reviewing types and type systems now because it will help us understand the new concepts being introduced in the next few pages. Before the introduction of object-oriented programming, programmers had to use other tools to build more complex data types than the primitives we’ve discussed here.

Subsections of Type Systems

Structs

Many object-oriented languages, such as C++ and C#, include the concept of a struct that form the basis of objects. A struct is an example of a compound data type, a data type composed from other types. This allows us to represent data in more complex ways by combining multiple primitive data types into a new type. This too, is a form of encapsulation, as it allows us to collect several values into a single data structure. Consider the concept of a vector from mathematics - if we wanted to store three-dimensional vectors in a program, we could do so in several ways. Perhaps the easiest would be as an array or list:

double[] vector = {3.0, 4.0, 5.0};
vector: List[float] = [3.0, 4.0, 5.0]

However, other than the variable name, there is no indication to other programmers that this is intended to be a three-element vector. And, if we were to accept it in a function, say a dot product, we’d need to check that the length of both arrays or lists was exactly 3:

public double dotProduct(double[] a, double[] b){
    if(a.length != 3 || b.length != 3){
        throw new IllegalArgumentException();
    }
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
}
def dot_product(a: List[float], b: List[float]) -> float:
    if len(a) != 3 or len(b) != 3:
        raise ValueError()
    return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]

A struct provides a much cleaner option, by allowing us to define a type that is composed of exactly three integers. Java and Python don’t directly support structs, but we can use classes with just variables and a constructor to mimic a struct in those languages:

public class Vector3{
    public double x;
    public double y;
    public double z;
    
    public Vector3(double x, double y, double z){
        this.x = x;
        this.y = y;
        this.z = z;
    }
}
class Vector3:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z

Then, our dot product method can take two arguments of the Vector3 type:

public double dotProduct(Vector3 a, Vector3 b){
    return a.x * b.x + a.y * b.y + a.z * b.z;
}
def dot_product(a: Vector3, b: Vector3) -> float:
    return a.x * b.x + a.y * b.y + a.z * b.z

There is no longer any concern about having the wrong number of elements in our vectors - it will always be three. We also get the benefit of having unique names for these fields (in this case, x, y, and z).

Thus, a struct allows us to create structure to represent multiple values in one variable, encapsulating the related values into a single data structure. We can then use those data structures as new data types in our program. Variables, and compound data types, together represent the state of a program. We’ll examine this concept in detail next.

Modules

It might seem like the kind of modules that Parnas was describing don’t exist in Java or Python, but they actually do - we just don’t call them “modules”. Consider how you would compute the square root of a number:

Math.sqrt(9.5);
math.sqrt(9.5)

The Math or math class in this example is actually used just like a module! We can’t see the underlying implementation of the sqrt() method, it just provides to us a well-defined interface (i.e. you call it with the symbol sqrt and a value as a parameter). This method and other related math functions are encapsulated within the Math or math class.

We can define our own module-like classes by making them static, i.e. we could group our vector math functions into a static VectorMath class.

import java.lang.Math;

public static class VectorMath(){
    
    public static double dotProduct(Vector3 a, Vector3 b){
        return a.x * b.x + a.y * b.y + a.z * b.z;
    }
    
    public static double magnitude(Vector3 a){
        return Math.sqrt(Math.pow(a.x, 2) + Math.pow(a.y, 2) + Math.pow(a.z, 2));
    }
}

Usage:

Vector3 vect1 = new Vector3(3.0, 4.0, 5.0);
Vector3 vect2 = new Vector3(6.0, 7.0, 8.0);
System.out.println(VectorMath.dotProduct(vect1, vect2));
System.out.println(VectorMath.magnitude(vect1));
import math

class VectorMath:
    
    @staticmethod
    def dot_product(a: Vector3, b: Vector3) -> float:
        return a.x * b.x + a.y * b.y + a.z * b.z
    
    @staticmethod
    def magnitude(a: Vector3) -> float:
        return math.sqrt(a.x ** 2 + a.y ** 2 + a.z ** 2)

Usage:

vect1: Vector3 = Vector3(3.0, 4.0, 5.0)
vect2: Vector3 = Vector3(6.0, 7.0, 8.0)
print(VectorMath.dot_product(vect1, vect2))
print(VectorMath.magnitude(vect2))

State and Behavior

YouTube Video

Video Materials

The data stored in a program at any given moment (in the form of variables, objects, etc.) is the state of the program. Consider a variable:

int a = 5;

The state of the variable a after this line is 5. If we then run:

a = a * 3;

The state is now 15. Consider the Vector3 struct we defined earlier. If we create an instance of that struct in the variable b:

Vector3 b = new Vector3(1.2, 3.7, 5.6);

The state of our variable b is {$1.2, 3.7, 5.6$}. If we change one of b’s fields:

b.x = 6.0;

The state of our variable b is {$6.0, 3.7, 5.6$}.

We can also think about the state of the program, which would be something like:

{$a: 5, b:${$x: 6.0, y: 3.7, z: 5.6$}}

We can therefore think of a program as a state machine. We can in fact, draw our entire program as a state table listing all possible legal states (combinations of variable values) and the transitions between those states. Techniques like this can be used to reason about our programs and even prove them correct!

This way of reasoning about programs is the heart of Automata Theory, a subject you may choose to learn more about if you pursue graduate studies in computer science.

What causes our program to transition between states? If we look at our earlier examples, it is clear that the assignment statement is a strong culprit. Expressions clearly have a role to play, as do control-flow structures, which decide which transformations take place. In fact, we can say that our program code is what drives state changes - the behavior of the program.

Thus, programs are composed of both state (the values stored in memory at a particular moment in time) and behavior (the instructions to change that state).

Now, can you imagine trying to draw the state table for a large program? Something on the order of EPIC?

On the other hand, with encapsulation we can reason about state and behavior on a much smaller scale. Consider this function working with our Vector3 struct:

public static Vector3 scale(Vector3 vec, double scale){
    double x = vec.x * scale;
    double y = vec.y * scale;
    double z = vec.z * scale;
    return new Vector3(x, y, z);
}
@staticmethod
def scale(vec: Vector3, scale: float) -> Vector3:
    x: float = vec.x * scale
    y: float = vec.y * scale
    z: float = vec.z * scale
    return Vector3(x, y, z)

If this method was invoked with a vector {$4.0, 1.0, 3.4$} and a scale $2.0$, our state table would look something like:

step vec.x vec.y vec.z scale x y z return.x return.y return.z
0 4.0 1.0 3.4 2.0 0.0 0.0 0.0 0.0 0.0 0.0
1 4.0 1.0 3.4 2.0 8.0 0.0 0.0 0.0 0.0 0.0
2 4.0 1.0 3.4 2.0 8.0 2.0 0.0 0.0 0.0 0.0
3 4.0 1.0 3.4 2.0 8.0 2.0 6.8 0.0 0.0 0.0
4 4.0 1.0 3.4 2.0 8.0 2.0 6.8 8.0 2.0 6.8

Because the parameters vec and scale, as well as the variables x, y, z, and the unnamed Vector3 we return are all defined only within the scope of the method, we can reason about them and the associated state changes independently of the rest of the program. This greatly simplifies both writing and debugging programs.

Subsections of State and Behavior

Classes and Objects

The module-based encapsulation suggested by Parnas and his contemporaries grouped state and behavior together into smaller, self-contained units. Alan Kay and his co-developers took this concept a step farther. Alan Kay was heavily influenced by ideas from biology, and saw this encapsulation in similar terms to cells.

Typical Animal Cell Typical Animal Cell1

Biological cells are also encapsulated - the complex structures of the cell and the functions they perform are all within a cell wall. This wall is only bridged in carefully-controlled ways, i.e. cellular pumps that move resources into the cell and waste out. While single-celled organisms do exist, far more complex forms of life are made possible by many similar cells working together.

This idea became embodied in object-orientation in the form of classes and objects. An object is like a specific cell. You can create many, very similar objects that all function identically, but each have their own individual and different state. The class is therefore a definition of that type of object’s structure and behavior. It defines the shape of the object’s state, and how that state can change. But each individual instance of the class (an object) has its own current state.

Let’s re-write our Vector3 struct using this concept.

public class Vector3{
    public double x;
    public double y;
    public double z;
    
    public Vector3(double x, double y, double z){
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    public double dotProduct(Vector3 other){
        return this.x * other.x + this.y * other.y + this.z * other.z;
    }
    
    public void scale(double scalar){
        this.x *= scalar;
        this.y *= scalar;
        this.z *= scalar;
    }
}
class Vector3:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        
    def dot_product(self, other: Vector3) -> float:
        return self.x * other.x + self.y * other.y + self.z * other.z
    
    def scale(self, scalar: float) -> None:
        self.x *= scalar
        self.y *= scalar
        self.z *= scalar

Here we have defined:

  1. The structure of the object state - three floating point values, x, y, and z
  2. How the object is constructed - the constructor that takes in parameters to set object’s initial state
  3. Instructions for how that object’s state can be changed, i.e. our scale() method

We can create as many objects from this class definition as we might want. Each one will have the same behavior but different state.

Vector3 one = new Vector3(1.0, 1.0, 1.0);
Vector3 up = new Vector3(0.0, 1.0, 0.0);
Vector3 a = new Vector3(5.4, -21.4, 3.11);
one: Vector3 = Vector3(1.0, 1.0, 1.0)
up: Vector3 = Vector3(0.0, 1.0, 0.0)
a: Vector3 = Vector3(5.4, -21.4, 3.11)

Conceptually, what we are doing is not that different from using a compound data type like a struct and a module of functions that work upon that struct. But practically, it means all the code for working with vectors appears in one place. This arguably makes it much easier to find all the pertinent parts of working with vectors, and makes the resulting code better organized and easier to maintain and add features to. This highlights why encapsulation is one of the key concepts in object-oriented programming.

Access Modifiers

YouTube Video

Video Materials

Access Modifiers in Python

Most of the content below will apply to the Java language only. Python does not directly support information hiding through access modifiers, but simulates it by allowing developers to prefix variables with underscores to indicate that they are “protected” and should be left alone. Likewise, prefixing a Python variable or method name with two underscores will make it appear private to the class, but a developer can simply add the class name to the variable or method name in order to access it. So, in places below where we state that an external class “cannot” access a private attribute, keep in mind that in Python it is always possible and “should not” is a better term to use.

Thankfully, the concepts are mostly the same, so this is good information for both Java and Python developers to understand.

Now let’s return to the concept of information hiding, and how it applies in object-oriented languages.

Unanticipated changes in state are a major source of errors in programs. Again, think back to the EPIC source code we looked at earlier. It may have seemed unusual now, but it used a common pattern from the early days of programming, where all the variables the program used were declared in one spot, and were global in scope (i.e. any part of the program could reassign any of those variables).

If we consider the program as a state machine, that means that any part of the program code could change any part of the program’s state. Provided those changes were intended, everything works fine. But if the wrong part of the state was changed, problems would ensue.

For example, if we were to make a typo in the part of the program dealing with water run-off in a field, which ends up assigning a new value to a variable that was supposed to be used for crop growth, we’ve just introduced a very subtle and difficult to find error. When the crop growth modeling functionality fails to work properly, we’ll probably spend serious time and effort looking for a problem in the crop growth portion of the code, but the problem doesn’t lie in that code at all!

Java, along with many other object-oriented languages, use access modifiers to implement data hiding. Consider a class representing a student:

public class Student{
    private String first;
    private String last;
    private int wid;
    
    public Student(String first, String last, int wid){
        this.first = first;
        this.last = last;
        this.wid = wid;
    }
}
class Student:
    
    def __init__(self, first: str, last: str, wid: int) -> None:
        self.__first = first
        self.__last = last
        self.__wid = wid

By using the access modifier private in Java, or prefixing the attributes with two underscores in Python, we have indicated that our fields first, last, and wid cannot be accessed (seen or assigned) outside of this code. For example, if we were to create a specific student:

Student willie = new Student("Willie", "Wildcat", 888888888);
willie: Student = Student("Willie", "Wildcat", 888888888)

We would not be able to change that student’s name. The statement willie.first = "Bob" would fail, because the field first is private. In fact, we cannot even see his name, so trying to print that value would also fail.

If we want to allow a field or method to be accessible outside of the object, we must declare it public in Java, or remove the underscores in Python. While we can declare fields public, this violates the core principles of encapsulation, as any outside code can modify our object’s state in uncontrolled ways. This is definitely not what we want.

Instead, in a true object-oriented approach we would write public accessor methods, a.k.a. getters and setters. These are methods that allow us to see and change field values in a controlled way. Adding accessors to our Student class might look like:

public class Student{
    private String first;
    private String last;
    private int wid;
    
    public Student(String first, String last, int wid){
        this.first = first;
        this.last = last;
        this.wid = wid;
    }
    
    public String getFirst(){
        return this.first;
    }
    
    public void setFirst(String value){
        if(value.length() > 0){
            this.first = value;
        }
    }
    
    public String getLast(){
        return this.last;
    }
    
    public void setLast(String value){
        if(value.length() > 0){
            this.last = value;
        }
    }
    
    public int getWid(){
        return this.wid;
    }
}
class Student:
    
    def __init__(self, first: str, last: str, wid: int) -> None:
        self.__first = first
        self.__last = last
        self.__wid = wid
        
    @property
    def first(self) -> str:
        return self.__first
    @first.setter
    def first(self, value: str) -> None:
        if len(value) > 0:
            self.__first = value
    
    @property
    def last(self) -> str:
        return self.__last
    @last.setter
    def last(self, value: str) -> None:
        if len(value) > 0:
            self.__last = value
            
    @property
    def wid(self) -> int:
        return self.__wid

Notice how the setFirst() and setLast() setters in Java, and the first() and last() setters in Python, check that the provided name has at least one character? We can use setters to make sure that we never allow the object state to be set to something that makes no sense.

Also, notice that the wid field only has a getter. This effectively means once a student’s wid is set by the constructor, it cannot be changed (it’s read only). This allows us to share data without allowing it to be changed outside of the class.

Getters and Setters vs. Properties

Notice that Java uses methods called getFirst and setFirst as getters and setters, while Python uses the @property decorator and methods that share the same name. These properties in Python simplify the use of getters and setters in code.

For example, in Java, if we want to use a getter or setter, we must call them by the function name:

willie.setFirst("William");
System.out.println(willie.getFirst());

Through the use of properties in Python, we can refer to the field directly by name, as if it were a public field, and our getter or setter will be called automatically:

willie.first = "William"
print(willie.first)

Unfortunately, Java does not support the use of properties at this time.

Subsections of Access Modifiers

Objects in Memory

We often talk about the class as a blueprint for an object. This is because classes define what properties and methods an object should have, in the form of a constructor. Consider this class representing a planet:

public class Planet{
    
    private double mass;
    public double getMass(){
        return this.mass;
    }
    
    private double radius;
    public double getRadius(){
        return this.radius;
    }
    
    public Planet(double mass, double radius){
        this.mass = mass;
        this.radius = radius;
    }
}
class Planet

    @property
    def mass(self) -> float:
        return self.__mass
    
    @property
    def radius(self) -> float:
        return self.__radius
    
    def __init__(self, mass: float, radius: float) -> None:
        self.__mass = mass
        self.__radius = radius

It describes a planet as having a mass and a radius, which will be stored as the ratio of this planet’s attribute compared to Earth. We can create a specific planet by invoking its constructor, i.e.:

Planet earth = new Planet(1.0, 1.0);
earth: Planet = Planet(1.0, 1.0)

In this example, earth is an instance of the class Planet. We can create other instances, i.e.

Planet mars = new Planet(0.107, 0.53);
mars: Planet = Planet(0.107, 0.53)

We can even create a Planet instance to represent one of the exoplanets discovered by NASA’s TESS:

Planet hd21749b = new Planet(23.20, 2.836);
hd21749b: Planet = Planet(23.20, 2.836)

Let’s think more deeply about the idea of a class as a blueprint. A blueprint for what, exactly? For one thing, it serves to describe the state of the object, and helps us label that state. If we were to check the radius of our variable mars, we would access the getter for the radius field:

mars.getRadius()
mars.radius

But a class does more than just labeling the properties and fields and providing methods to mutate the state they contain. It also specifies how memory needs to be allocated to hold those values as the program runs.

Looking at our Planet class again, we can see it contains two floating point values. So, when we run the constructor for that class, our computer will know that it needs to allocate enough space in memory for those two values (8 bytes each in Java, and 24 bytes each in Python).

State and memory are clearly related - the current state is what data is stored in memory. It is possible to take that memory’s current state, write it to persistent storage (like the hard drive), and then read it back out at a later point in time and restore the program to exactly the state we left it with. This is actually what your operating system does when you put it into hibernation mode.

The process of writing out the state is known as serialization, and it’s a topic we’ll revisit later.

Static Modifier

You might have wondered how the static modifier plays into objects. Essentially, the static keyword indicates the field or method it modifies exists in only one memory location. I.e. a static field references the same memory location for all objects that possess it.

Consider this simple example class:

public class Simple:
    public static int x;
    public int y;
    
    public Simple(int x, int y){
        this.x = x;
        this.y = y;
    }
}
class Simple:
    
    x: int = 0
        
    def __init__(self, x: int, y: int) -> None:
        Simple.x = x
        self.y = y

Notice that the Java language uses the static keyword for fields, whereas in Python the field is simply defined outside of the constructor, and only attached to the class name and not self.

We can also create a couple of instances:

Simple first = new Simple(10, 12);
Simple second = new Simple(8, 5);
first: Simple = Simple(10, 12)
second: Simple = Simple(8, 5)

Once we’ve created both instances, the value of first.x would be 8 - because first.x and second.x reference the same memory location (a static unchanging location), and second.x was set after first.x. If we changed it again:

first.x = 3

Then both first.x and second.x would have the value 3, as they share the same memory location. first.y would still be 12, and second.y would still be 5.

Another way to think about static is that it means the field or method we are modifying belongs to the class and not the individual object. Hence, each object shares a static variable, because it belongs to their class.

Used on a method, the static keyword in Java or the @staticmethod decorator in Python indicates that the method belongs to the class definition, not the object instance. Hence, we must invoke it from the class, not an object instance: i.e. Math.pow().

Finally, when used with a class in Java, static indicates we can’t create objects from the class - the class definition exists on its own. Hence, the Math m = new Math(); is actually an error, as the Math class is declared static. Python does not directly support the static keyword for classes themselves, but classes which only contain static attributes and methods could be considered static classes.

Message Passing

YouTube Video

Video Materials

The second criteria Alan Kay set for object-oriented languages was message passing. Message passing is a way to request a unit of code engage in a behavior, i.e. changing its state, or sharing some aspect of its state.

Consider the real-world analogue of a letter sent via the postal service. Such a message consists of: an address the message needs to be sent to, a return address, the message itself (the letter), and any data that needs to accompany the letter (the enclosures). A specific letter might be a wedding invitation. The message includes the details of the wedding (the host, the location, the time), an enclosure might be a refrigerator magnet with these details duplicated. The recipient should (per custom) send a response to the host addressed to the return address letting them know if they will be attending.

In an object-oriented language, message passing primarily take the form of methods. Let’s revisit our example Vector3 class from earlier:

public class Vector3{
    public double x;
    public double y;
    public double z;
    
    public Vector3(double x, double y, double z){
        this.x = x;
        this.y = y;
        this.z = z;
    }
    
    public double dotProduct(Vector3 other){
        return this.x * other.x + this.y * other.y + this.z * other.z;
    }
    
    public void scale(double scalar){
        this.x *= scalar;
        this.y *= scalar;
        this.z *= scalar;
    }
}
class Vector3:
    
    def __init__(self, x: float, y: float, z: float) -> None:
        self.x = x
        self.y = y
        self.z = z
        
    def dot_product(self, other: Vector3) -> float:
        return self.x * other.x + self.y * other.y + self.z * other.z
    
    def scale(self, scalar: float) -> None:
        self.x *= scalar
        self.y *= scalar
        self.z *= scalar

We can also create a couple of instances of the class, and use its dot product method:

Vector3 a = new Vector3(1.0, 1.0, 2.0);
Vector3 b = new Vector3(4.0, 2.0, 1.0);
double c = a.dotProduct(b);
a: Vector3 = Vector3(1.0, 1.0, 2.0)
b: Vector3 = Vector3(4.0, 2.0, 1.0)
c: float = a.dot_product(b)

Consider the invocation of a.dotProduct(b) (Java) or a.dot_product(b) (Python) above. The method name, dotProduct or dot_product provides the details of what the message is intended to accomplish (the letter). Invoking it on a specific variable, i.e. a, tells us who the message is being sent to (the recipient address). The return type indicates what we need to send back to the recipient (the invoking code), and the parameters provide any data needed by the class to address the task (the enclosures).

Let’s define a new method for our Vector3 class that emphasizes the role message passing plays in mutating object state:

public void normalize(){
    double magnitude = Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
    this.x /= magnitude;
    this.y /= magnitude;
    this.z /= magnitude;
}
def normalize(self) -> None:
    magnitude: float = math.sqrt(self.x ** 2 + self.y ** 2 + self.z ** 2)
    self.x /= magnitude
    self.y /= magnitude
    self.z /= magnitude

We can now invoke the normalize() method on a Vector3 object to mutate its state, shortening the magnitude of the vector to length 1.

Vector3 f = new Vector3(9.0, 3.0, 2.0);
f.normalize();
f: Vector3 = Vector3(9.0, 3.0, 2.0)
f.normalize()

Note how here, f is the object receiving the message normalize. There is no additional data needed, so there are no parameters being passed in. Our earlier dot product method took a second vector as its argument, and used that vector’s values to mutate its state.

Message passing therefore acts like those special molecular pumps and other gate mechanisms of a cell that control what crosses the cell wall. The methods defined on a class determine how outside code can interact with the object. An extra benefit of this approach is that a method becomes an abstraction for the behavior of the code, and the associated state changes it embodies. As a programmer using the method, we don’t need to know the exact implementation of that behavior - just what data we need to provide, and what it should return or how it will alter the program state. This makes it far easier to reason about our program, and also means we can change the internal details of a class (perhaps to make it run faster) without impacting the other aspects of the program.

Function vs. Method

You probably have noticed that in many programming languages we speak of functions, but in Java and other object-oriented languages, we’ll often speak of methods. You might be wondering just what is the difference?

Both are forms of message passing, and share many of the same characteristics. Broadly speaking though, methods are functions defined as part of an object. Therefore, their bodies can access the state of the object. In fact, that’s what the this keyword in Java means - it refers to this object, i.e. the instance of the class that the method is currently executing for. In Python, any class methods include a parameter typically named self that represents the same concept - the instance of the class that the method was called on. For non-object-oriented languages, there is no concept of this (or self as it appears in other languages).

However, many times developers will use the terms function and method interchangeably. Likewise, variables stored in a class may be referred to as both attributes and fields. Sadly, we are not very exacting about how we use our own terms, even though our field requires us to be exacting in other ways. So, we’ll just have to do our best to read the context clues and interpret what is meant. In this book, we’ll try to use these terms as clearly as we can.

Subsections of Message Passing

Summary

In this chapter, we looked at how object-orientation adopted the concept of encapsulation to combine related state and behavior within a single unit of code, known as a class. We further explored how objects are instances of a class created through invoking a constructor method.

We also discussed several different ways of looking at and reasoning about objects - as a state machine, and as structured data stored in memory. We discussed how a method is really a form of message passing that provides an interface to interact with objects safely.

Finally, we explored how all of these concepts are implemented in both the Java and Python programming languages.

Review Quiz

Check your understanding of the new content introduced in this chapter below - this quiz is not graded and you can retake it as many times as you want.

Quizdown quiz omitted from print view.