程式扎記: [ In Action ] The Simple Groovy datatypes - The concept of optional typing & Overriding operators

標籤

2013年12月26日 星期四

[ In Action ] The Simple Groovy datatypes - The concept of optional typing & Overriding operators

Preface: 
We haven’t used any explicit static typing in the way that you’re familiar with in Java. We assigned strings and numbers to variables and didn’t care about the type. Behind the scenes, Groovy implicitly assumes these variables to be of static type java.lang.Object. This section discusses what happens when a type is specified, and the pros and cons of static and dynamic typing. 

Assigning types: 
Groovy offers the choice of assigning types explicitly just as you do in Java. Table 3.3 gives examples of optional static type declarations and the dynamic type used at runtime. The def keyword is used to indicate that no particular type is demanded. 
 

It is important to understand that regardless of whether a variable’s type is explicitly declared, the system is type safe. Unlike untyped languages, Groovy doesn’t allow you to treat an object of one type as an instance of a different type without a well-defined conversion being available. For instance, you could never treat ajava.lang.String with value “1” as if it were a java.lang.Number, in the hope that you’d end up with an object that you could use for calculation. That sort of behavior would be dangerous—which is why Groovy doesn’t allow it any more than Java does. 

Static versus dynamic typing: 
Static typing provides more information for optimization, more sanity checks at compile-time, and better IDE support; it also reveals additional information about the meaning of variables or method parameters and allows method overloading. Static typing is also a prerequisite for getting meaningful information from reflection. 

Dynamic typing, on the other hand, is not only convenient for the lazy programmer who does some ad-hoc scripting, but also useful for relaying and duck typing. Suppose you get an object as the result of a method call, and you have to relay it as an argument to some other method call without doing anything with that object yourself: 
  1. class Duck{  
  2.     void quack(){println "Qqqqqqq";}  
  3.     void fly(){println "I can't fly";}  
  4. }  
  5.   
  6. class Bird{  
  7.     void quack(){println "Yayaya";}  
  8.     void fly(){println "I can fly";}  
  9. }  
  10.   
  11. void work(def obj)  
  12. {  
  13.     obj.quack();  
  14.     obj.fly();  
  15. }  
  16.   
  17. work(new Duck())  
  18. work(new Bird())  
In this case, you’re not interested in finding out what the heck the actual type and package name of that objs are. You are spared the work of looking them up, declaring the type, and importing the package. You also communicate: “That’s just something which can fly() and quack().” The second usage of dynamic typing is calling methods on objects that have no guaranteed type. This is often called duck typing, and we will explain it in more detail in section 7.3.2. This allows the implementation of generic functionality with a high potential of reuse. 

Overriding operators: 
Overriding refers to the object-oriented concept of having types that specify behavior and subtypes that override this behavior to make it more specific. When a language bases its operators on method calls and allows these methods to be overridden, the approach is called operator overriding

It’s more conventional to use the term operator overloading, which means almost the same thing. The difference is that overloading suggests that you have multiple implementations of a method (and thus the associated operator) that differ only in their parameter types. We will show you which operators can be overridden, show a full example of how overriding works in practice, and give some guidance on the decisions you need to make when operators work with multiple types. 

Overview of overridable operators: 
As you saw previously, 1+1 is just a convenient way of writing 1.plus(1) . This is achieved by class Integer having an implementation of the plus method. This convenient feature is also available for other operators. Table 3.4 shows an overview. 
 
 

 
Ps. Strictly speaking, Groovy has even more operators in addition to those in table 3.4, such as the dot operator for referencing fields and methods. Their behavior can also be overridden. They come into play in chapter 7. 

Overridden operators in action: 
Listing 3.1 demonstrates an implementation of the equals == and plus + operators for a Money class. It is a low-level implementation of the Value Object pattern. We allow money of the same form of currency to be added up but do not support multicurrency addition. 

We implement equals such that it copes with null comparison. This is Groovy style. The default implementation of the equals operator doesn’t throw anyNullPointerExceptions either. Remember that == (or equals ) denotes object equality (equal values), not identity (same object instances). 
  1. package inaction.ch3  
  2.   
  3. class Money {  
  4.     private int     amount  
  5.     private String  currency  
  6.     Money (amountValue, currencyValue) {  
  7.         amount   = amountValue  
  8.         currency = currencyValue  
  9.     }  
  10.       
  11.     // Overload '==' operator  
  12.     boolean equals (Object other) {  
  13.         if (null == other)              return false  
  14.         if (! (other instanceof Money)) return false  
  15.         if (currency != other.currency) return false  
  16.         if (amount   != other.amount)   return false  
  17.         return true  
  18.     }  
  19.     int hashCode() {  
  20.         amount.hashCode() + currency.hashCode()  
  21.     }  
  22.       
  23.     // Implement '+' operator  
  24.     Money plus (Money other) {  
  25.         if (null == other)    return null  
  26.         if (other.currency != currency) {  
  27.             throw new IllegalArgumentException(  
  28.                 "cannot add $other.currency to $currency")  
  29.         }  
  30.         return new Money(amount + other.amount, currency)  
  31.     }  
  32.       
  33.     static void main(args)  
  34.     {  
  35.         def buck = new Money(1'USD')  
  36.         assert buck  
  37.         assert buck        == new Money(1'USD')  
  38.         assert buck + buck == new Money(2'USD')  
  39.     }  
  40. }  
To explain the difference between overriding and overloading, here is a possible overload for Money’s plus operator. In listing 3.1, Money can only be added to otherMoney objects. In case, we would like to add Money as: 
  1. assert buck + 1 == new Money(2'USD')  
We can provide the additional method: 
  1. Money plus (Integer more) {  
  2.     return new Money(amount + more, currency)  
  3. }  
that overloads the plus method with a second implementation that takes an Integer parameter. The Groovy method dispatch finds the right implementation at runtime. 

Making coercion work for you: 
Implementing operators is straightforward when both operands are of the same type. Things get more complex with a mixture of types, say 
This adds an Integer and a BigDecimal. What is the return type? Section 3.6 answers this question for the special case of numbers, but the issue is more general. One of the two arguments needs to be promoted to the more general type. This is called coercion

When implementing operators, there are three main issues to consider as part of coercion. 

Supported argument types 
You need to decide which argument types and values will be allowed. If an operator must take a potentially inappropriate type, throw an IllegalArgumentExceptionwhere necessary. For instance, in our Money example, even though it makes sense to use Money as the parameter for the plus operator, we don’t allow different currencies to be added together. 

Promoting more specific arguments 
If the argument type is a more specific one than your own type, promote it to your type and return an object of your type. To see what this means, consider how you might implement the plus operator if you were designing the BigDecimal class, and what you’d do for an Integer argument. 

Integer is more specific than BigDecimal : Every Integer value can be expressed as a BigDecimal , but the reverse isn’t true. So for the BigDecimal.plus(Integer)operator, we would consider promoting the Integer to BigDecimal ,performing the addition, and then returning another BigDecimal —even if the result could accurately be expressed as an Integer

Handling more general arguments with double dispatch 
If the argument type is more general, call its operator method with yourself (“this,” the current object) as an argument. Let it promote you. This is also called double dispatch, and it helps to avoid duplicated, asymmetric, possibly inconsistent code. Let’s reverse our previous example and consider Integer.plus(BigDecimal operand)

We would consider returning the result of the expression operand.plus(this) , delegating the work to BigDecimal’s plus(Integer) method. The result would be aBigDecimal, which is reasonable—it would be odd for 1+1.5 to return an Integer but 1.5+1 to return a BigDecimal

Groovy’s conventional behavior 
Groovy’s general strategy of coercion is to return the most general type. Other languages such as Ruby try to be smarter and return the least general type that can be used without losing information from range or precision. The Ruby way saves memory at the expense of processing time. It also requires that the language promote a type to a more general one when the operation would generate an overflow of that type’s range. Otherwise, intermediary results in a complex calculation could truncate the result. 

Supplement: 
[ Java 小學堂 ] Java 世界裡 equals 與 == 的差別

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!