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:
- class Duck{
- void quack(){println "Qqqqqqq";}
- void fly(){println "I can't fly";}
- }
- class Bird{
- void quack(){println "Yayaya";}
- void fly(){println "I can fly";}
- }
- void work(def obj)
- {
- obj.quack();
- obj.fly();
- }
- work(new Duck())
- work(new Bird())
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).
- package inaction.ch3
- class Money {
- private int amount
- private String currency
- Money (amountValue, currencyValue) {
- amount = amountValue
- currency = currencyValue
- }
- // Overload '==' operator
- boolean equals (Object other) {
- if (null == other) return false
- if (! (other instanceof Money)) return false
- if (currency != other.currency) return false
- if (amount != other.amount) return false
- return true
- }
- int hashCode() {
- amount.hashCode() + currency.hashCode()
- }
- // Implement '+' operator
- Money plus (Money other) {
- if (null == other) return null
- if (other.currency != currency) {
- throw new IllegalArgumentException(
- "cannot add $other.currency to $currency")
- }
- return new Money(amount + other.amount, currency)
- }
- static void main(args)
- {
- def buck = new Money(1, 'USD')
- assert buck
- assert buck == new Money(1, 'USD')
- assert buck + buck == new Money(2, 'USD')
- }
- }
- assert buck + 1 == new Money(2, 'USD')
- Money plus (Integer more) {
- return new Money(amount + more, currency)
- }
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
- 1 + 1.0
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 與 == 的差別
沒有留言:
張貼留言