---- 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. Another advantage of generics over the "object" supertype is speed. There is no runtime type casting involved, and there is also no need to "box and unbox", that is, convert between primitive and reference types. 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; } 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? Assume you wish to have a single list that consists of some sub1 objects and some sub2 objects. Which class would you use? -- 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 = new two(); two n = new two(); two n2 = new two(); n.next = n2; 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. one v = new one(); v.item = "hello"; // a string is an object two v.next = new one(); // link to another list v.next.item = new bankaccount(100); // different type of object One can also have this type of list using class two by instantiating A with object: two, in which case it would have the same behavior as one. 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. 6b. Given : interface I { void f(int x); } interface J { void g(int x); } class A : J { int x = 0; void g(int x) { x++; Console.WriteLine(x); } } Class A objects do not implement the I interface. However, sometimes we may want to use an A object as an I object (instead of a J object). Write an object adapter class that enables this. That is, calling f on an adopted A object should invoke A's g function. class Iadapter : I { private A adaptee; // object to be adopted public Iadapter(A x) {adaptee=x;} // constructor public void f(int x) { A.g(x); } // adopts interface J to I } ............ 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.