---- Answers to sample questions to prepare for the final exam ----
1. Given the following:
interface I
{
I f();
}
class A : I
{
public virtual I f() { return this; }
}
Explain what's wrong with the following lines:
A x = new A();
A y = x.f();
>>>
The second line will not compile because the return type of x.f() is I,
not A. Explicit casting is required from subclass to superclass.
If we had written A y = (A)x.f(), then it would compile since
A is a subclass of I. It would also run in this case since x is indeed
an A object.
2. In the visitor pattern we had a critical function:
public object accept(visitor v) { return v.visit(this); }
Where was this function defined (in what class(es) does it exist?
visitor or visitee?)
What does "this" refer to?
>>>
The function is placed in every visitee subclass. "this" refers to
the visitee, the data object to be visited. A subtle point about this
function is that, if you look at our examples such as the food visitors,
you'll see that there's no accept in the "foodbase" superclass. Why
not put it in the base superclass, and just have every subclass inherit it?
It's because of "this" - the type of "this" in foodbase would be foodbase,
and not one of the subclasses (meat, fruit, vegetable). The visitor objects
can only visit one of these specific subclasses.
3. Describe a situation in which an object adapter (aka adapter pattern)
can be useful. Can you think of a situation outside of the food
program?
>>>
When you have two classes that have incompatible interfaces, and want
to consider one type of object as another type of object. For
example:
lets say you have
Interface POINT
{
int xcord();
int ycord();
}
class point : POINT
{ int x, y;
int xcord() {return x;}
int ycord() {return y;}
}
but the following class is not related:
class circle
{
int x, y, radius;
}
Now suppose somewhere you have a function
public static double distance(POINT A, POINT B)
{
int dx = A.xcord() - B.xcord();
int dy = A.ycord() - B.ycord();
return Math.Sqrt(dx*dx+dy*dy);
}
Suppose now you wish to call the distance function on circles instead
of points. You can't do it directly because the distance function
expects points, not circles. In order to avoid having to rewrite the
distance function, you must ADAPT a circle to a point:
class pointadapter : Point
{
private circle c;
public pointadapter(circle x) { c=x; }
int xcord() { return c.x; }
int ycord() { return c.y; }
}
The adapter must implement all the functions of the interface it is adapting
an object to.
Now to find the distance between two circels c1, and c2, you can call
... distance(new pointadapter(c1), new pointadapter(c2));
4. Explain the difference between natural and artificial polymorphism.
Give an example of each.
>>>
Polymorphism is first of all a characteristic of the algorithm, not of
the program or programming language. Some algorithms, such as finding
the length of a linked list, are polymorphic by nature since it never
looks at what's stored in the linked lists. Some algorithms, such as
sorting a list, also have the potential to work for many kinds of
lists, but only if we supply the appropriate definition of what it
means for two elements to be "<" and "==" to eachother. This kind of
procedure are artificially polymorphic. Programming languages have
mechanisms that allow you to build this kind of abstraction.
Inheritance is one method. One can define different procedures for
"<" and "==" in subclasses. In "simpler" languages such as Scheme,
Perl or pure C, one can achieve this kind of polymorphism by passing a
function parameter to the procedure. For example, one that defines
"<" for a sorting procedure.
Please understand this by putting it in your own words. Don't just
memorize it. See question 6 below (hint: it's asking the same question,
but from a different angle).
5. Explain in your own words one advantage of parametric polymorphism
over inheritance polymorphism. That is, what's the difference between
using a type parameter , and calling your variables (for example)
"A x" instead of "object x".
>>>
The basic problem with the inheritance approach to polymorphism is that
we loose the ability to type check code at compile time. As you should
know well by now, many errors are only runtime errors, not compile time
errors. At compile time, all that is known is the superclass of a variable,
not its actual type. If we type a variable as "object", then it could be
anything and we effectively loose all ability to catch type errors statically.
Generics, or parametric types, give us the ability to type check code
at compile time. That is, we define a parametric class such as
class yourclass
{ ...
but when we use it, we have to instantiate it with an actual type, as in
yourclass x = ...
yourclass y = ...
Thus the compiler can see the specific type information for each
object. Parametric types are not always better than inheritance,
since inheritance lets you define different versions of procedures for
different subtypes. But sometimes we use "object x" to mean that x can
be of any type, and this is really not the right use of inheritance.
5b. Assume that classes sub1 and sub2 both extend class super1. Consider the
following alternatives for defining a polymorphic linked list class
that can contain elements of either sub1 or sub2:
class one
{
super1 item; // head, car
one next; // tail, cdr
}
class two where A : super1
{
A item;
two next;
}
Write code to construct an instance of class two where item is
of type sub1.
two m = new two();
Assume you wish to have a linked list M of sub1 objects and another list
N of sub2 objects. Which class would you use and why?
Since the two lists will only contain objects of the same type,
I would use the parametric version for better error reporting and
better efficiency (due to lack of run-time type casts and checks).
two m = ...
two n = ...
However, if the lists can contain mixed elements, or if it is desired
that the type of referenced objects can change from one type to another,
then I would use the first method.
6. Some people are of the opinion that untyped languages such as Scheme and
Perl are better for implementing polymorphism, and that types just get
in the way. That is, in Scheme for example, one can define the length
function as:
(define (length l) (if (null? l) 0 (+ 1 length(cdr l))))
There's no mention of types and obviously the function will work with
any kind of list. Types, even parametric types, simply gets in the
way of programming. Regardless of whether you agree with this opinion,
there are issues that this view is not taking into consideration.
What are they?
>>>
There are two major problems:
1. without types, we loose the ability to catch many errors at compile
time by allowing non-logically structured programs to run.
2. For procedures that are naturally polymorphic this opinion has merit.
One need not be worried about supplying type information in
Scheme/Perl. But "artificial" polymorphism requires one to build
a layer of abstraction above different algorithms. Having a typeless
language won't help here - you'll still have to supply the different
algorithms (e.g. for both integer and rational equality).
The role of parametric polymorphism (generics) is not clear cut. The
advantage it has over the inheritance method is that it allows static
type checking, using the type inference rules we talked about.
However, parametric polymorphism won't allow you to *build*
abstraction either. That is, using a type variable alone won't
resolve the differences between integer and rational equality, for
example. One must still provide different algorithms. Thus parametric
polymorphism is only useful in implementing routines that are already
(naturally or rendered) polymorphic. In other words, in untyped
languages such as Scheme or Python it's trivial to write naturally
polymorphic functions. In a typed language, one still wishes to write
such functions, but in a way that's consistent with the type system
and its advantages. This is the dilemma that parametric polymorphism
attempts to address.
............
Sample AspectJ problems:
7. Assume that classes C and D both have a function void f(int).
Write a pointcut expression that picks out calls to either function. Also
capture the argument passed.
>>>
(call(void C.f(int)) || call(void D.f(int))) && args(x);
8. Given class:
class B
{
private int x;
pubilc B(int x0) { x=x0; } // constructor
public int f(int n)
{ if (n<2) return 1; else return n*f(n-1); }
public void g(int y)
{ System.out.println(x+f(y)); }
}
a. Write a pointcut that picks out the "execution" of the constructor of B.
(when the constructor is called, the object doesn't exist yet).
>>> execution(B.new(..))
remember that the constructor has no return type (not even void). Also
note that the .. means that it applies to any constructor.
b. Write a pointcut that picks out the initial call to f (as opposed to
recursive calls).
>>> call(int B.f(int)) && !withincode(int B.f(int))
c. The following pointcut and advice tries to change the parameter passed to
g. Explain what's wrong with it the way it's written. DON'T JUST CORRECT
IT; *EXPLAIN* WHY IT'S WRONG THE WAY IT IS!
before(int y) : call(void B.g(int)) && args(y)
{
g(y+1);
}
(hint: there are three problems that need to be addressed).
>>>
1. A before advice won't prevent the original computation from being
carried out, unless it throws an exception. This should be an
around advice.
2. The object (instance of B) that g was called on was not captured.
Need to use the "target" pointcut
3. The call to g within the advice will cause the advice to activate
again, causing an infinite loop.
The correct way to write this would be
void around(int y, B n): call(void B.g(int)) && args(y)
&& target(n) && !adviceexecution
{
n.g(y+1);
}
or
void around(int y, B n): call(void B.g(int)) && args(y)
&& target(n) && !within(nameofaspect)
{
n.g(y+1);
}
or
void around(int y, B n): call(void B.g(int)) && args(y) && target(n)
{
proceed(y+1,n);
}
// note that proceed always take the same params as the advice, regardless
// of what the method g takes. It just means "proceed with the following
// information from the given pointcut".
d. Write an advice that throws an error if the g function is called from
anywhere except main (public static void *.main(..))
before() : call(void B.g(int)) && !withincode(public static void *.main(..))
{
throw new Error("can't call from there")
}
---
d. Write an advice that throws an error if the g function is called from
anywhere except main (public static void *.main(..))
>>
before() : call(void B.g(int)) && !withincode(public static void *.main(..))
{
throw new Error("can't do that!");
}
f. Suppose an aspect contains the following intertype declarations:
Aspect importantaspect
{
public int B.y;
private int B.z;
...
Explain the difference between public and private above (hint: they're
not the same as public/private within an ordinary class). That is,
what are the consequences with respect to other parts of the program.
>>
The public declaration makes B.y visible to the entire program (that's
compiled together with the aspect)
The private declaration is visible only to the aspect. Note, this is
not the same as "adding a private variable to class B". You cannot write
code in class B that will try to use z. The word "private" pertains to the
aspect, not the class B.
---
9. Explain the difference between the "withincode" and "cflow" pointcuts.
>>>
withincode examines the static source code of a function, whereas cflow
is concerned with what happens during run time. for example, if
we have
void f()
{ g(); }
void g()
{ h(); }
and f(); is called.
then the call to h from g will be recognized by cflow(call(void *.f()))
but not by withincode(void *.f()). Only the call to g will be recognized
by withincode(void *.f()) since statically f evidently calls g. (g is
called while executing the source code of f).
Also remember the difference between signature pointcuts and property
pointcuts. That is, never use something like withincode and cflow
alone. Use them only in conjunction with call/execution/set/get, or something
that defines the join point more precisely.
10. Explain the difference between the "this" and "target" pointcuts.
The best way to answer this question is by using a specific example.
>>>>
target is the object that the method call was invoked on. "this" refers
to the calling object. If the called method is static, there is no target
object. Similarly, if the method is called from a static function such as
main, there is no "this" object.
class A
{
void f()
{ B n = new B();
n.g();
}
}
class B
{
void g() { ... }
}
...
A m = new A();
m.f();
In this context, the following advice form:
before(A x, B y) : call(void B.g()) && this(x) && target(y)
{ ... }
will capture m as x and n as y.