PATTERNS09 - Generics in .NET and Java

24
Generics Michael Heron

Transcript of PATTERNS09 - Generics in .NET and Java

GenericsMichael Heron

Introduction

There is a common technique available in

Java (versions 1.5 and above) and .net

(version 2 and above).

The Generic Datatype

Overloading and polymorphism go a long

way towards making an object oriented

system work ‘properly’

But they only take you to the bridge.

The Problem

Let’s say we want to create a basic data

structure.

One that works for any object.

It’s something straight forward – a queue,

for example.

How do we do that?

Well, in older versions of a language we’d

declare the data type as an Object.

The Queue Public class Queue {

ArrayList<Object> myObjects;

void addToQueue (Object ob) {myObjects.add (ob);

}

void removeFromQueue () {Object ob = myObjects.get(0);myObjects.remove (0);return ob;

}}

The Problem We can use casting to turn whatever is on the

queue into whatever class we need:Person p =

(Person)queue.removeFromQueue();

This is bad OO. It’s not type safe

We need to enforce discipline to make sure that we don’t put the wrong things on the wrong queues. The compiler should be doing as much of that

as is feasible.

The Solution

Both C# and Java offer the Generic as a

solution.

A class which acts as a type-safe template.

The syntax is a little awkward, but it allows

us to define the type that should be

associated with a data structure.

Like we do with ArrayLists.

Queue – Java Genericimport java.util.*; public class Queue<T> {

ArrayList<T> myList; public Queue() {

myList = new ArrayList<T>(); } public void addToQueue(T ob) {

myList.add(ob); } public T getFromQueue() {

T ob = myList.get(0); myList.remove(0); return ob;

}

public boolean hasMoreElements() { return (myList.size() != 0);

} }

Queue – Java Genericpublic static void main(String args[]) {

Queue<String> myQueue = new Queue<String>();

myQueue.addToQueue("Hello!"); myQueue.addToQueue("World!"); while (myQueue.hasMoreElements()) {

System.out.println(myQueue.getFromQueue());

} }

Queue – C# Genericclass Queue<T>

{

ArrayList myObjects;

public Queue ()

{

myObjects = new ArrayList();

}

public void addToStack<T>(T ob)

{

myObjects.Add(ob);

}

public T removeFromStack()

{

T ob = (T)myObjects[0];

myObjects.RemoveAt(0);

return ob;

}

}

}

Why Use Generics?

Type safe – we can ensure type incompatibilities are dealt with at the earliest possible opportunity.

Simplifies syntax – no need to cast individual objects.

Allows for effective deployment of certain kinds of design patterns.

Avoids the need for excessive specialisation of classes.

How do they work?

It is important to know the different ways

in which variables are bound during the

running of an application.

Traditionally variables are bound to a

specific context in one of two ways.

Static binding, which is done at compile

time.

Dynamic binding, which is done at runtime.

Static Binding

Explicitly indicating the type of a

parameter allows for the compiler to link

objects and variables when compiled.

They’re not going to change in that

respect.

The performance of this is high, and

compile-time checking can be rigorous in

a way that’s not possible otherwise.

Late Binding

Late binding is used extensively in Java and C#.

One key area in which it is used is in polymorphism.

When you use Polymorphism, Java adopts a late binding approach so that it can properly adapt to the object at runtime.

It knows the most specialised method to use when invoked, but only when the object is bound.

Strongly Typed Languages In strongly typed languages, early binding is

the norm. We can tell what the context is going to be by

analysing the runtime

However, late binding needs to be dealt with either through polymorphism or compile time casting.

Generics allow for you to defer the binding of a data type until its point of usage arrives. The <T> parameter is unbound.

When we instantiate the class, we bind it to a specific context.

Boxing and Unboxing

In both Java and C# a related

mechanism is known as autoboxing.

This is the process of converting a value

variable into an object reference, or vice

versa.

When a value reference is boxed, it is

stored on the ‘managed heap’.

A chunk of memory set aside and tended

by the garbage collector.

Wrapper Classes

Each primitive data type in Java and C#

comes with a corresponding wrapper

class.

A class designed to provide a way of

dealing with it as a reference.

It used to be impossible to have an

ArrayList of ints in Java.

You needed to make them Integer objects

first.

Wrapper Classes

Autoboxing then is the process at play when a primitive data type is encapsulated within a wrapper.

And vice versa, when it is unwrapped into its primitive form.

Autoboxing is a relatively expensive process.

If you were doing this a lot, it would be worth assessing your specific data manipulation requirements.

Generics and Constraints

All of this leads to an obvious problem.

What if we don’t want everything to be on

the table for a generic?

Luckily, generics allow us to set constraints

on them.

Limitations that restrict what can be a valid

specification of our class.

There are six types of these in .NET.

ConstraintsConstraint Description

Where T: struct The type argument must be a value type.

Where T: class The type argument must be a reference

type.

Where T: new() The type argument must have a public,

parameterless constructor

Where T: <class name> The type argument must extend from the

indicated class name.

Where T: <interface

name>

The type argument must implement the

specified interface, or be the interface itself

Where T: U The type argument for T must be or derive

from the argument supplied for U.

Exampleclass Queue<T> where T : IComparable

{

ArrayList myObjects;

public Queue ()

{

myObjects = new ArrayList();

}

public void addToStack<T>(T ob)

{

myObjects.Add(ob);

}

public T removeFromStack()

{

T ob = (T)myObjects[0];

myObjects.RemoveAt(0);

return ob;

}

public Boolean isInQueue(T ob)

{

T ob2;

for (int i = 0; i < myObjects.Count; i++) {

ob2 = (T)myObjects[i];

if (ob2.CompareTo(ob) != -1) {

return true;

}

}

return false;

}

}

Constraints

Multiple constraints can be applied to the same parameter.

And in turn, they can be generic in and of themselves.

If you are going to be performing operations on a type that are not defined in Object itself, you need to apply a constraint.

That will allow for the method to be made available in a type-safe way.

Multiple Parameters

Some classes may provide two types.

For example, Hashtables

T and U are used conventionally to refer

to parameter 1 and parameter 2.

You can apply separate constraints to

each of these:

Where U : class

Where T : iComperable

Unconstrained Types

With unconstrained types, we have the

following restrictions:

We cannot use simple logical comparators

on them, because there is no guarantee

the concrete type will support them.

They will need to be formally cast.

You can compare to null, but this will

always return false if the type argument is a

value type.

Conclusion

Generics offer a new and powerful way to deal with type-safe collections.

And other kinds of classes.

Constraints allow us to ensure that we can access useful methods as required.

Polymorphism will ensure that we can reliably access whatever internals we require.

They’re available in both C# and Java.