4. Basics of Object-Oriented Programming

The topics discussed in this chapter are considered the basics of object-oriented programming. Some of them are language-independent, others are specific to C#, Java, or Scala. These topics, except perhaps for the last two, are supposed to be covered by the CS 1/2 prerequisite chain for this course.

Todo

update all examples and references — meanwhile, please go here

4.1. Reference semantics vs. value semantics

  • Value semantics: variables directly contain values.

  • Reference semantics: variables contain addresses that refer (point) to objects.

4.1.2. References

  • Java

  • C# (see also Effective C# item 6)

4.2. Equality vs. identity

  • Equality: are two (possibly) different objects equivalent?

  • Identity: do two references refer to the same objects?

  • How are equality and identity related?

  • Reconciling value and reference semantics: identity of objects explained as equality of addresses.

4.2.2. References

4.3. Parametric polymorphism (generics)

Familiar from data structures course:

  • without generics: Stack (of objects); loose typing

  • without generics: IntStack, StringStack, BookStack; strict typing but lots of duplicate boilerplate code

  • with generics: Stack<Int>, Stack<String>, Stack<Book>; strict typing without code duplication

Relatively easy to use, can be challenging to incorporate correctly in the design of one’s abstractions.

4.3.2. References

4.4. Relationships among classes and interfaces

These are common relationships among concepts and are part of UML's class diagrams.

4.4.1. Class/interface-level relationships

These relationships are between classes and/or interfaces, so they cannot change at run time.

From strong to weak:

  • is-a, realizes-a: generalization/specialization (subtyping)

  • uses-a: dependency

4.4.2. Instance-level relationships

These relationships are between instances, so they can change at run time.

From strong to weak:

  • owns-a: composition

  • part-of: aggregation

  • has-a or other specific relationship: association

4.4.3. Examples

UML class diagram representing a taxonomy of vehicles

A UML class diagram representing a taxonomy of vehicles.

4.5. Class-interface continuum

  • Concrete class (C++, C#, Java, Scala): can be instantiated. All specified methods are fully implemented.

  • Abstract class (C++, C#, Java, Scala): cannot be instantiated. Some or all of the specified methods are not implemented. A class cannot extend more than one abstract class.

  • Trait (Scala only): cannot be instantiated directly. Some or all of the specified methods are not implemented. A class or trait can extend zero or more traits, and member lookup is automatically disambiguated based on trait order (see traits and mixins for details).

  • Interface (Java, C# only): limit case of a fully abstract class for specification purposes only. None of the specified methods are implemented, and there are no instance variables.

Related to the single-responsibility and interface-segregation principles.

4.5.2. References

4.6. Subtyping vs. subclassing/inheritance

  • Subtyping allows substituting a more specific object for a more general one, for example, when passed as an argument or assigned to a variable.

  • Inheritance is a mechanism for a subclass to reuse state and behavior from a superclass.

    • inherit methods and fields

    • add fields

    • add or replace/refine methods

  • Inheriting from a superclass enables weak syntactic subtyping. (In some languages, this relationship can be public or nonpublic.)

  • The Liskov Substitution Principle (LSP) defines strong semantic (behavioral) subtyping.

  • Implementing or extending an interface also enables syntactic subtyping (and semantic subtyping because interfaces have no behavior). Extending a trait also enables syntactic subtyping.

4.6.2. References

4.7. Subtype polymorphism: static vs. dynamic type

  • Static type: declared type of a variable.

  • Dynamic type: actual type of the object to which the variable refers.

  • Dynamic method binding: x.f(a1, a2, ...). Two steps:

    1. Verify whether receiver x supports method f based on static type.

    2. Search for version of f to be invoked starting from dynamic type and proceeding upward until found.

  • How are static and dynamic type of a variable related?

  • If step 1 succeeds, will step 2 always succeed as well?

  • Casting: treat an object as if it had a different static type. Three different situations: - downcast - upcast - crosscast

  • Overloading versus overriding. - @Override/override correctness in Java and Scala

4.7.2. References

4.8. Being a good descendant of java.lang.Object/System.Object

Classes are usually required to provide the following methods (these specific ones are for Java):

  • toString (for displaying instances in a meaningful way)

  • equals (if an instance can be in an equivalence class that include other instances)

  • hashCode (ditto)

  • compareTo (if instances are ordered)

  • clone (if instances are mutable)

  • close (if instances are closeable resources)

Also related to the Liskov substitution principle.

4.8.2. References

4.9. Clone in the context of the Composite pattern

In general, cloning allows you to make a copy of an object. The clone method in Java is similar to the copy constructor in C++, but it is an ordinary method, unlike the copy constructor. Once you have the original object and its clone, then you can modify each one independently. Accordingly, cloning is necessary only if the objects are mutable.

Cloning models the real-life situation where you build a prototype of something, say a car or a piece of furniture, and once you like it, you clone it as many times as you want. These things are composites, and the need to be cloned deeply (recursively).

As another example, imagine a parking garage with a list of cars that have access to it. To build another garage to handle the growing demand, you can clone the garage and the customer access list. But the (physical) cars should not get cloned. That’s because the garage is not composed of the cars.

As we can see, the conceptual distinction between aggregation and composition has significant consequences for the implementation of the relationship. True, both relationships are represented as references in Java. However, composites usually require a deep clone (if cloning is supported) where each parent is responsible for cloning its own state and recursively cloning its children.

As mentioned above, you don’t need to support cloning at all if your objects are immutable because you wouldn’t be able to distinguish the original from the clone anyway.

4.9.1. References

4.10. Packages/namespaces

  • Mechanism for grouping related or collaborating classes (cf. default package-level member access).

  • In Java, implemented as mapping from fully qualified class names to file system. In Scala, this is much looser.

  • In addition, in Java, each public class must be in a separate file whose name matches the class name.

4.10.1. Examples

4.10.2. References

4.11. Member access

  • public

  • protected

  • default (package)

  • private

Related to the information hiding and open-closed principles.

4.11.1. References