Implementing Object Equivalence in Java Using the Template Method Design Pattern

Similar documents
Canonical Form. No argument constructor Object Equality String representation Cloning Serialization Hashing. Software Engineering

Methods Common to all Classes

Graphical Interface and Application (I3305) Semester: 1 Academic Year: 2017/2018 Dr Antoun Yaacoub

Inheritance and Polymorphism

What s Conformance? Conformance. Conformance and Class Invariants Question: Conformance and Overriding

Contents. I. Classes, Superclasses, and Subclasses. Topic 04 - Inheritance

CS/ENGRD 2110 FALL Lecture 6: Consequence of type, casting; function equals

CSE 401/M501 Compilers

Java Fundamentals (II)

Super-Classes and sub-classes

Expected properties of equality

CSE 331 Software Design & Implementation

Outline. Inheritance. Abstract Classes Interfaces. Class Extension Overriding Methods Inheritance and Constructors Polymorphism.

OBJECT ORİENTATİON ENCAPSULATİON

Chapter 6 Introduction to Defining Classes

Introduction to Object-Oriented Programming

Java for Programmers Course (equivalent to SL 275) 36 Contact Hours

CSE 331 Software Design & Implementation

Patterns for polymorphic operations

Software Construction

Part 3. Why do we need both of them? The object-oriented programming paradigm (OOP) Two kinds of object. Important Special Kinds of Member Function

Comp 248 Introduction to Programming Chapter 4 & 5 Defining Classes Part B

Weiss Chapter 1 terminology (parenthesized numbers are page numbers)

Introduction to Programming Using Java (98-388)

PROGRAMMING LANGUAGE 2

Inheritance. Inheritance Reserved word protected Reserved word super Overriding methods Class Hierarchies Reading for this lecture: L&L

Building Java Programs. Inheritance and Polymorphism

Computer Science II (20073) Week 1: Review and Inheritance

CSE P 501 Compilers. Implementing ASTs (in Java) Hal Perkins Autumn /20/ Hal Perkins & UW CSE H-1

Programming Languages and Techniques (CIS120)

Item 4: Extensible Templates: Via Inheritance or Traits?

Outline. Logistics. Logistics. Principles of Software (CSCI 2600) Spring Logistics csci2600/

CMSC131. Inheritance. Object. When we talked about Object, I mentioned that all Java classes are "built" on top of that.

Polymorphism. return a.doublevalue() + b.doublevalue();

Outline. Java Models for variables Types and type checking, type safety Interpretation vs. compilation. Reasoning about code. CSCI 2600 Spring

Inheritance -- Introduction

Day 4. COMP1006/1406 Summer M. Jason Hinek Carleton University

Java Inheritance. Written by John Bell for CS 342, Spring Based on chapter 6 of Learning Java by Niemeyer & Leuck, and other sources.

Comp 249 Programming Methodology

Comp 249 Programming Methodology Chapter 8 - Polymorphism

ITI Introduction to Computing II

Unit3: Java in the large. Prepared by: Dr. Abdallah Mohamed, AOU-KW

CS558 Programming Languages

Inheritance, Polymorphism, and Interfaces

Inheritance. Inheritance allows the following two changes in derived class: 1. add new members; 2. override existing (in base class) methods.

Programming II (CS300)

The Java Type System (continued)

Chapter 4 Defining Classes I

CS-202 Introduction to Object Oriented Programming

CSE P 501 Compilers. Implementing ASTs (in Java) Hal Perkins Winter /22/ Hal Perkins & UW CSE H-1

Safety SPL/2010 SPL/20 1

Instantiation of Template class

4 CoffeeStrainer Virtues and Limitations

The Object Class. java.lang.object. Important Methods In Object. Mark Allen Weiss Copyright 2000

Late-bound Pragmatical Class Methods

What is Inheritance?

Software Development (cs2500)

ITI Introduction to Computing II

Lesson 10A OOP Fundamentals. By John B. Owen All rights reserved 2011, revised 2014

Chapter 5 Object-Oriented Programming

Increases Program Structure which results in greater reliability. Polymorphism

Comp 249 Programming Methodology

CSE 331 Spring 2018 Midterm

Making New instances of Classes

CS/ENGRD 2110 FALL Lecture 6: Consequence of type, casting; function equals

Agenda. Objects and classes Encapsulation and information hiding Documentation Packages

Data abstractions: ADTs Invariants, Abstraction function. Lecture 4: OOP, autumn 2003

DESIGN PATTERN - INTERVIEW QUESTIONS

CH. 2 OBJECT-ORIENTED PROGRAMMING

B16 Object Oriented Programming

Programming Language Concepts Object-Oriented Programming. Janyl Jumadinova 28 February, 2017

Cpt S 122 Data Structures. Course Review Midterm Exam # 2

Inheritance (Extends) Overriding methods IS-A Vs. HAS-A Polymorphism. superclass. is-a. subclass

C++ Programming: Polymorphism

Lecture 10. Overriding & Casting About

More on Design. CSCI 5828: Foundations of Software Engineering Lecture 23 Kenneth M. Anderson

CE221 Programming in C++ Part 1 Introduction

Equality. Michael Ernst. CSE 331 University of Washington

CS/ENGRD 2110 SPRING 2018

Inheritance and Polymorphism

Wrapping a complex C++ library for Eiffel. FINAL REPORT July 1 st, 2005

Domain-Driven Design Activity

A Short Summary of Javali

Object Oriented Programming. Java-Lecture 11 Polymorphism

COP 3330 Final Exam Review

Goals of Lecture. Lecture 27: OO Design Patterns. Pattern Resources. Design Patterns. Cover OO Design Patterns. Pattern Languages of Programming

CS558 Programming Languages Winter 2013 Lecture 8

Fast Introduction to Object Oriented Programming and C++

Object Oriented Programming is a programming method that combines: Advantage of Object Oriented Programming

Programming in C++ Prof. Partha Pratim Das Department of Computer Science and Engineering Indian Institute of Technology, Kharagpur

Equality for Abstract Data Types

25. Generic Programming

Exception Handling Alternatives (Part 2)

1B1b Inheritance. Inheritance. Agenda. Subclass and Superclass. Superclass. Generalisation & Specialisation. Shapes and Squares. 1B1b Lecture Slides

Rules and syntax for inheritance. The boring stuff

More On inheritance. What you can do in subclass regarding methods:

Introduction to Inheritance

Java: introduction to object-oriented features

What are the characteristics of Object Oriented programming language?

COURSE 2 DESIGN PATTERNS

Transcription:

Implementing Object Equivalence in Java Using the Template Method Design Pattern Daniel E. Stevenson and Andrew T. Phillips Computer Science Department University of Wisconsin-Eau Claire Eau Claire, WI 54701 {stevende, phillipa@uwec.edu Abstract A standard practice in object-oriented programming is to implement an operation, called equals in Java, for testing the equality of two objects. The equals method should be defined for every new Java class, but because of the intricacies of inheritance, casting, and dynamic typing, equals is often quite difficult to write correctly. And unfortunately many textbooks present flawed implementations of this operation. In this paper, we present a semantically correct technique for testing object equivalence, a technique that simultaneously brings together important mathematical foundations (equivalence relations), practical programming issues (inheritance, casting, dynamic typing), and sound software design (design patterns) in a natural and compelling way. While Java is used to demonstrate how the semantic flaws are corrected and the design improved using our techniques, the design is general enough that it will be clear how the same ideas could easily be extended to other languages such as C++. 1 Introduction An increasingly common design tool for the practicing software designer is the framework, since frameworks allow programmers to write applications that easily leverage code written by others. For example the GUI Applet framework gives programmers the ability to perform complex graphical operations without ever opening a window, while data structure frameworks such as the Java Collections Framework (JCF) and the Standard Template Library (STL) allow programmers to use complex sets, linked lists, and trees without having to worry about the data structure implementation details. Of course, frameworks also come with expectations on the objects they manipulate. For example, both the JCF and STL expect that testing for object equality be implemented as an equivalence relation, and failure to do so can cause Sets to contain duplicate values. Such errors are notoriously difficult to find, even for experienced programmers. And to make matters worse, textbooks often present flawed versions of these critical operations. In this paper, we present a way of using design patterns to ensure that framework requirements are met. In particular, we focus on testing for object equality. Note, however, that similar arguments can also be made for comparison. Our main point here is twofold: first, that there is a semantically right way to test for equality of objects, and second that such a design easily admits (in fact, almost demands) the use of the Template Method design pattern. Our purpose over the next few sections is to develop a design, based on the Template Method design pattern [4], for testing object equality that simultaneously maximizes code reuse, encapsulation, and data hiding, and minimizes semantic errors. We begin by considering a typical, but flawed, beginner s implementation of object equality. We then show how even in this most simple and fundamental of examples, the Template Method design pattern is an easy design solution that corrects these flaws. We present our ideas in Java, but the generality of our design also makes it easily transferable to C++. 2 Testing for Equality using the Template Method Design Pattern For our discussions that follow, we assume the existence of a base class A containing, for simplicity, just a single private data member that would be properly initialized by a default constructor (which we do not show). Given this definition, consider the beginner s implementation of the simple, but flawed, equals member function shown below: Beginner s implementation of equals class A { public boolean equals(a that) { return (this.x == that.x);

As a teaching tool, this example is a universal favorite because it forces new Java developers to carefully confront their understanding of the apparent versus actual types of objects when they attempt to use this method in a code fragment such as the one that follows. For this and subsequent examples, assume that the default constructor sets x to some value. Attempting to use equals from class A, but invoking equals from Object instead Object instance1 = new A(); Object instance2 = new A(); if (instance1.equals(instance2) { // should be equal, but won t be! Of course, in order to force the Java virtual machine to correctly select the equals method provided by class A (versus that provided by the class Object), the argument type defined in the signature for equals must be Object, and that s usually the point of emphasis when teaching new Java programmers. While our example is tailored to make this point, Java programmers will experience this error in practice when comparing Objects extracted from a Vector or any of the collections in the JCF. Either way, the end result is the same the wrong code will be executed. But the story hardly ends there. There is in fact a right way ([2], [3], [5]), and a number of wrong ways, to write the entire equals member function. In fact, there are hundreds of examples in the standard Java library containing flawed implementations of equals. What criteria should a properly constructed equals method satisfy? There are three critical criteria (in fact, there are more than three criteria, but we ll focus on just these three): 1 equals must have the signature exactly as follows: boolean equals(object). 2 equals must return false if its argument is null. 3 equals must be an equivalence relation, meaning that it satisfies 3.1 Reflexivity: a.equals(a) must be true. 3.2 Symmetry: If a.equals(b) then b.equals(a). 3.3 Transitivity: If a.equals(b) and b.equals(c) then a.equals(c). Given these criteria, the following implementation involving a new super class T and the original class A, which is now a sub class of T, properly satisfies these requirements: Equals and localequals using a super class T abstract class T { public final boolean equals(object that) { if ((that!= null) && (that instanceof T)) { T castedthat = (T) that; if (this.gettypeequiv().equals( castedthat.gettypeequiv())) { isequal = localequals(that); return true; // to stop the chaining abstract protected Class gettypeequiv(); class A extends T { A castedthat = (A) that; boolean isequal = (this.x == castedthat.x); return (isequal && super.localequals(that)); protected Class gettypeequiv() { Class result = null; try { // will never fail, but must try/catch result = Class.forName( A ); catch (ClassNotFoundExeception e) { return result; Notice that our implementation factors out the actual private data member comparisons into a new protected mode helper function that we ve called localequals. Doing so results in an implementation of equals that is independent of any specific class, and so we have moved it into the abstract super class called T. Hence, our form of equals is invariant with respect to class, and so can be inherited by all sub classes of T without modification! And that s why we made it public and final. Also of note is that the only way for two instances to be equal is if their corresponding types are the same, and if they both inherit from the super class T. From a software design standpoint, we would argue that if two objects do not both inherit from T, their types shouldn t be considered equivalent, and so they would not be equal. Hence, each sub class will necessarily be required to supply a unique (and protected) version of localequals and of gettypeequiv in order to make the proper private data member comparisons and to report the appropriate class type, respectively. If we now extend the class A with a sub class B also containing (again for simplicity) a single private data member y, here would be the versions of localequals and

gettypeequiv for class B, assuming that we do not wish for A and B to be of the same type: LocalEquals and gettypeequiv for class B class B extends A { B castedthat = (B) that; boolean isequal = (this.y == castedthat.y); return (isequal && super.localequals(that)); protected Class gettypeequiv() { Class result = null; try { // will never fail, but must try/catch result = Class.forName( B ); catch (ClassNotFoundExeception e) { return result; private int y; Notice that our skeletal design for localequals differs for each class in two areas only. First, each class requires a specific downcast of the argument into the appropriate class type (e.g., B castedthat = (B) that). This cast is type safe because the actual types of both this and that are previously checked and guaranteed to be the same by the calls to gettypeequiv in the equals method of our super class T. At the moment, gettypeequiv simply returns the appropriate actual types using Class.forName for each separate class, but we ll revisit this particular issue later to handle a more general situation (and also explain why we did not use getclass or instanceof here instead). Second, each class must provide for the specific comparisons of the private data members for that class. Note that the skeletal design we have provided explicitly chains to the super class for any local comparisons required there as well. Hence, we avoid any need for accessors or (worse) for direct access to super class data members. This design nicely enforces encapsulation, data hiding, and code reuse. Figure 1 shows the UML for our new design: - x : int A -y :int T + equals(object that) : boolean B Figure 1: Template Method Design for equals We ve really done nothing more than apply the Template Method design pattern [4] to testing object equivalence while guaranteeing that the code also satisfies the general contract for equivalence relations. The template in this case is our class T with its equals method since it supplies the invariant steps required to correctly implement equality. The variant code is supplied by each sub class in localequals and gettypeequiv, and we have even provided a basic skeletal outline for each such method. 3 Some Common Design Errors We also wish to point out that many versions of equals do not guarantee that equals implements an equivalence relation. Specifically, the symmetry requirement of criteria 3.2 is often unsatisfied. Here is a variation of the suspect code that can be found in almost any Java programming text and in many Java library routines: Equals using instanceof for classes A and B class A { public boolean equals(object that) { if ((that!= null) && (that instanceof A)) { A castedthat = (A) that; isequal = (this.x == castedthat.x); class B extends A { public boolean equals(object that) { if ((that!= null) && (that instanceof B)) { B castedthat = (B) that; isequal = (this.y == castedthat.y); return (isequal && super.equals(that)); private int y; Note that the equals method is overridden in each class and sub class since the use of the cast testing operator instanceof requires a specific type to test. The problem with this version (aside from the duplicated code) is highlighted by the following code segment, which shows the failure in the symmetry requirement (see also [1], [2], [3], and [5]): Failure of symmetry requirement using instanceof Object instancea = new A(); Object instanceb = new B(); if ((instancea.equals(instanceb)) == (instanceb.equals(instancea))) { // should be true, but will be false! While instanceb is an instanceof class B and therefore of class A, instancea is not an instanceof class B. So,

instancea.equals(instanceb) would return true, but instanceb.equals(instancea) would fail the instanceof test and return false. Our solution corrects that deficiency by using Class.forName inside of gettypeequiv to retrieve the actual type of the arguments instead of using the cast testing instanceof operator. At the same time we move the variant type-casting code into the localequals functions for each class. A 1 A 2 T B 2 B 1 B 4 4 Defining Type Equivalence for Classes In our Template Method design, each class specific implementation of localequals requires a specific downcast of the argument into the appropriate type, and as described earlier this cast is type safe because the actual types of both this and that are previously checked and guaranteed to be the same by the use of Class.forName in the code for gettypeequiv. Obviously if this and that are of the same class, then they are of identical type, and this is a key to why our design correctly adheres to the general contract for equivalence relations (in particular, the symmetry and transitivity requirements). What if one wanted to define a set of two or more different classes all of which should be treated as the same type? For example, it might be desirable to extend class A with class B in such a way as to permit overriding of some of the methods of A, but still maintain identical private data members. Or more generally, it might be desirable to simply define two different classes A and B both of which inherit from a common ancestor C, neither of which inherits from the other, and yet consider A and B to be equivalent types for the purposes of comparison. We call this concept type equivalence. In either case our design permits these generalizations as long you adhere to the following simple rules: 1. All classes that are in the same type equivalence set E must have a common ancestor R E in the inheritance hierarchy, and R E also must be in the set E. Hence, R E is the canonical member for the type equivalent classes in E. 2. The template class T must be a proper ancestor of R E in the inheritance hierarchy. 3. R E must be the only member of the type equivalence set E to define gettypeequiv and localequals. All other members of E will therefore inherit these methods from R E. 4. If A and B are both members of the type equivalence set E, then all classes between A and B in the inheritance hierarchy must also be in E. As an example of how these rules will permit a variety of designs of type equivalence while maintaining semantic correctness, encapsulation, and code reuse, consider the example inheritance hierarchy shown in Figure 2. B 3 Figure 2: A Type Equivalence Inheritance Hierarchy Figure 2 shows three distinct type equivalence sets: A = {A 1, A 2, B = {B 1, B 2, B 3, B 4, and C = {C 1. Each type equivalence set has a top-most member (i.e. the canonical member R E ) of the inheritance hierarchy: these are A 1 for A, B 1 for B, and C 1 for C in this example. Each of these is therefore required to define a version of localequals and of gettypeequiv, but none of the other members of the inheritance hierarchy in this example would contain such methods. In particular, the sub classes B 2, B 3, and B 4 would all inherit localequals and gettypeequiv from B 1 since they are all in the same type equivalence set as B 1. Using this design allows two instances of the type equivalence set B to be tested for equality based only on the private data members of B 1. The fact that the two distinct instances may not be of the same actual Java type is of no consequence here since the downcast from an Object to a B 1 performed in localequals is still type safe (as guaranteed by gettypeequiv). And of course the semantic correctness of equals is still maintained by our Template Method design. As a concluding remark on the design and implementation of equals with respect to type equivalence, if you take the position that each distinct class should define its own distinct type equivalence set (and the authors do indeed take this position), then the design is greatly simplified. In this case, gettypeequiv would be moved into the template class T and made private and final as shown below. Ensuring that no two distinct classes are type equivalent abstract class T {... // same as before C 1 private final Class gettypeequiv() { return this.getclass(); In this way, each class identifies itself (using getclass) as distinct from all other classes, and there is no need or way for any class to ever override gettypeequiv. 5 Transferability of the Design Pattern to C++ One test of the value and generality of a good design is whether the same design is cleanly transferable to a different object-oriented language. In the case of our equals design and C++, we are interested in the overloading of the operator==.

Many of the same design and implementation issues we considered in Java are also issues in C++, namely providing the proper signatures, testing the argument for null, ensuring this and that are type equivalent and then performing the corresponding downcast, and most importantly, ensuring that the operation actually implements an equivalence relation. In each case, there is a simple, but not always elegant, C++ solution. As an example, here would be the C++ versions of the template base class T and the sub class A from section 2 above, but now implemented in C++. C++ versions of classes T and A class T { public: bool operator==(const T &that) const { bool isequal = false; if ((&that!= NULL) && (*(this->gettypeequiv()) == *(that.gettypeequiv()))) { isequal = (this->localequals(&that)); protected: virtual bool localequals(const T *that) const { return true; // default to stop the chaining ; virtual const type_info* gettypeequiv() const = 0; class A : public T { protected: virtual bool localequals(const T *that) const { const A *castedthat = dynamic_cast<const A*>(that); bool isequal = (this->x == castedthat->x); return (isequal && T::localEquals(that)); 6 Summary We have presented a semantically correct software design solution for testing object equivalence using the Template Method design pattern. But we have done considerably more as well. We have taken this simple but important example and demonstrated how identifying variant and invariant behavior can be the key to code reuse and the proper implementation of equality. We have demonstrated how to use dynamic typing and inheritance to properly chain sub class methods to their super classes in order to obtain the correct semantic behavior. We have shown how to satisfy the general contract for equivalence relations, provide for flexible definitions of type equivalence, and yet not require any code duplication. And we have highlighted the importance of defining the proper method signatures in order to obtain the desired dynamic method invocation behavior. References [1] Bergin, J. Canonical Form of a Java Class. http://csis.pace.edu/~bergin/patterns/canonicaljava.ht ml. [2] Bloch, J. Effective Java Programming Language Guide. Addison-Wesley (2001). [3] Cohen, T. How Do I Correctly Implement the equals() Method? Dr. Dobb s Journal, May (2002). [4] Gamma, E., Helm, R., Johnson, R., and Vlissides, J. Design Patterns: Elements of Reuseable Object- Oriented Software. Addison-Wesley Publishing (1995). [5] Horstmann, C., and Cornell, G. Core Java 2: Volume I Fundamentals. Sun Microsystems Press (2001). [6] Stevenson, D.E., and Phillips, A.T. A Template Design for Correctly Overriding the Operator== in C++. UW- Eau Claire Computer Science Technical Report, August (2002). virtual const type_info* gettypeequiv() const { return &typeid(*(new A())); private: int x; ; The design similarities between the C++ version and our Java version are intentional and apparent. The only significant change in the design of the code (ignoring the obvious syntax and implementation specific details of type casting) is that the C++ version of operator== requires the template type T to be the argument in the signature, since C++ has no overarching Object. This also permits us to drop the explicit that instanceof T inheritance check since this type compatibility test is required for compilation anyway. Aside from this, the generality of our design makes it easily transferable to C++. For a complete discussion about these and many more issues in the C++ implementation of operator==, see [6].