Learning a new programming language is a twofold task: learning the syntax and learning the standard library. Whereas learning the syntax is a matter of days and getting proficient with new language idioms may require a matter of a few weeks, working through a new library can easily take several months.
Luckily, no Java programmer needs to go through this time-consuming activity when learning Groovy. They already know most of the Groovy Standard Library, because that is the set of APIs that the Java Runtime provides. You can work with Groovy by solely using objects and methods as provided by the Java platform, although this approach doesn’t fully leverage the power of Groovy.
Groovy extends (and in a few places modifies) the JRE to make it more convenient to work with, provide new dynamic features, and adapt the APIs to Groovy language idioms. The total of these extensions and modifications is called the GDK. Figure 9.1 gives the architectural overview.
Figure 9.1 GDK’s place in the Groovy architecture
A big part of the GDK concerns the datatypes that Groovy supports at the language level, such as strings, numbers, lists, and maps. That part of the GDK was covered in part 1 of this book. Here focuses on GDK capabilities that come from extending prominent JDK concepts such as Object , File , Stream , Thread , Process , and text processing with templates.
Working with Objects:
Java comes with a narrow API of 11 methods for its central abstraction java.lang.Object. These methods deal with the object lifecycle ( clone , finalize ), object equality (equals , hashCode ), information ( toString ), self-reflection ( getClass ), and multithreading support ( notify , notifyAll , three versions of wait ).
Groovy adds much to the self-reflective and informational aspects of the API to better support live exploration of objects. It handles identity/equality differently and therefore needs to extend the respective API. It adds convenience methods to Object for the purpose of making these methods available anywhere in the code. Finally, it adds collection-aware methods to Object that are useful when the object can be seen as some kind of collection even though it is not necessarily of static typejava.util.Collection. This last category also includes the handling of object arrays.
Interactive objects:
When working on a program, you often need to inspect your objects, whether for debugging, logging, or tracing purposes. In dynamic languages such as Groovy, this need is even greater, because you may work with your programming language in an interactive fashion, asking your objects about their state and capabilities to subsequently send them messages.
Object information in strings
Often, the first task is to ask an object for some general information about itself: toString() in Java parlance. Groovy adds two more methods of this kind:
Listing 9.1 shows these methods called on a string that contains a single newline character.
- Listing 9.1 Usage of dump and inspect
- def newline = "\n"
- assert newline.toString() == "\n"
- assert newline.dump() ==
- '''<java.lang.String@a value=
- offset=0 count=1 hash=10>'''
- assert newline.inspect() == /'\n'/
- groovy.inspect.swingui.ObjectBrowser.inspect(obj)
You have seen the dump method reveal the object’s fields and their values. The same and more can be done with the object’s properties.
Accessing properties
Remember that any Groovy object can be seen as a JavaBean, as you saw in section 7.4. You have already seen that its properties can be inspected with thegetProperties method or the properties property. The method returns a read-only map of property names and their current values. During inspection, printing the whole map of properties is as easy as:
- println properties // Or println someObj.properties
Listing 9.2 shows property reflection in use. The example uses a class with a first property and a second read-only property that returns a derived value and is not backed by a field. A third property is only a field without accessor methods. The listing shows how to list all keys of that object’s properties.
- Listing 9.2 Reflecting on properties
- class MyClass {
- def first = 1 // read-write property
- def getSecond() { first * 2 } // read-only property
- public third = 3 // public field property
- }
- obj = new MyClass()
- keys = ['first','second','class']
- assert obj.properties.keySet() == new HashSet(keys)
- // Properties map
- assert 1 == obj.properties['first']
- assert 1 == obj.properties.first
- // 1) Direct access
- assert 1 == obj.first
- assert 1 == obj['first'] // getAt('first')
- one = 'first'
- two = 'second'
- // 2) Dynamic assignment
- obj[one] = obj[two] // putAt(one)
- // 3) Field introspection
- assert obj.dump() =~ 'first=2'
Note.
Working with properties means working on a higher level of abstraction than working with methods or even fields directly. We will now take one step down and look at dynamic method invocation.
Invoking methods dynamically
In the Java world, methods (and fields) belong to Class rather than to Object. This is appropriate for most applications of reflection, and Groovy generally follows this approach. When you need information about an object’s methods and fields, you can use the following GPath expressions:
- obj.class.methods.name
- obj.class.fields.name
- obj.metaClass.metaMethods.name
Groovy follows a slightly different approach than Java when it comes to invoking these methods dynamically. You saw the invokeMethod functionality for GroovyObjects in section 7.1.2. The GDK makes this functionality ubiquitously available on any (Java) object. In other words, a Groovy programmer can call on any arbitrary object:
- object.invokeMethod(name, params)
Dynamic method invocation is useful when the names of the method or its parameters are not known before runtime!
Consider this scenario: You implement a persistence layer with Data Access Objects (DAOs), or objects that care for accessing persistent data. A Person DAO may have methods like findAll , findByLastName , findByMaximum , and so on. This DAO may be used in a web application setting as depicted in figure 9.2. It may respond to HTTP requests with request parameters for the type of find action and additional parameters. This calls for a way to dispatch from the request parameters to the method call.
Such a dispatch can be achieved with if-else or switch constructions. Listing 9.3 shows how dynamic method invocation makes this dispatching logic a one-liner. Because this is only an illustrative example, we return the SQL statement strings from the DAO methods, not the Person objects as we would probably do in real DAOs.
- Listing 9.3 Dynamic method invocation in DAOs
- class PersonDAO {
- String findAll() {
- 'SELECT * FROM Person'
- }
- String findByLastname(name) {
- findAll() + " WHERE p.lastname = '$name'"
- }
- String findByMaximum(attribute) {
- findAll() + " WHERE $attribute = " +
- "SELECT maximum($attribute) FROM Person"
- }
- }
- dao = new PersonDAO()
- action = 'findAll' // some external input
- params = [] as Object[]
- assert dao.invokeMethod(action, params) == 'SELECT * FROM Person'
As a second example, an external configuration in plain-text files, tables, or XML may specify what action to take under certain circumstances. Think about domain specific languages (DSLs), the specification of a finite state machine, a workflow description, a rule engine, or a Struts configuration. Dynamic method invocation can be used to trigger such actions.
These scenarios are classically addressed with the Command pattern. In this pattern, dynamic invocation can fully replace simple commands that only encapsulate actions (that is, they don’t encapsulate state or support additional functionality like undo).
While we are on this topic, dynamic invocation can be applied not only to methods but also to closures. In order to select a closure by name, you can store such closures in properties. An idiomatic variant of listing 9.3 could thus be:
- class PersonDAO {
- public findAll = {
- 'SELECT * FROM Person'
- }
- // more finder methods as Closure fields ...
- }
- dao = new PersonDAO()
- action = 'findAll' // some external input
- params = []
- assert dao[action](*params) == 'SELECT * FROM Person'
These variants differ slightly in size where the closure variant is a bit shorter but may be less readable for the casual Groovy user. The closure variant additionally offers the possibility of changing the closure at runtime by assigning a new closure to the respective field. This can be handy in combination with the State pattern or Strategy pattern!
Convenient Object methods:
How often have you typed System.out.println when programming Java? In Groovy, you can achieve the same result with println , which is an abbreviation for this.println; and because the GDK makes println available on Object , you can use this anywhere in the code. This is what we call a convenience method.
This section walks through the available convenience methods and their usage, as listed in table 9.1.
Because Groovy uses the == operator for equality instead of identity checking, you need a replacement for the Java meaning of == . That is what the is method provides. In Java:
- if ( a == b ) { /* more code here */}
- if ( a.is(b)) { /* more code here */}
The isCase method occurred often in the Groovy language description in part 1. For Object, the GDK provides a default implementation that checks for object equality. Note that this means you can use any (Java) object in a Groovy grep or switch:
- switch(new Date(0)){
- case new Date(0) : println 'dates are equal'
- }
- new Date().identity {
- println "$date.$month.$year"
- }
The last convenience method in our list is sleep , which suspends the current thread for a given number of milliseconds. It enhances the JDK method Thread.sleep by automatically handling interruptions such that sleep is re-called until the given time has elapsed (as closely as the machine timer can tell). This makes the effective sleep time more predictable.
If you want to handle interruptions differently, you can attach a closure that is called when sleep encounters an InterruptedException. With the sleep method, you can have some fun, as with the following example. Run it from the Groovy shell or console after predicting its output. Did you guess correctly what it does?
- text = """
- This text appears
- slowly on the screen
- as if someone would
- tpye \b\b \b\b \b\b \bype it.
- """
- for (c in text) {
- sleep 100
- print c
- }
Iterative Object methods:
In the Java world, any collection (in the general meaning of the word) of objects can support inspection of its contained items by providing an Iterator , a separate object that knows how to walk through that collection. Oh—wait, sometimes an Enumeration is used instead. As a further inconsistency, Iterators are not directly available on arrays and a lot of other common types.
Even if you are lucky and found how to get an Iterator object in the API documentation, you cannot do much more with it than use it in a procedural way like this:
- // Java !
- for (Iterator collection.iterator(); iterator.hasNext(); ){
- MyClass obj = (MyClass) iterator.next();
- // do something with obj
- }
- collection.each { /* do something with it */}
What’s so useful about the methods in table 9.2 is that you can use them on any object you fancy. The GDK makes these methods available on Object and yields the respective items. As we described in section 6.3.2, this iteration strategy is also used in Groovy’s for loop.
Getting the items is done with a best-effort strategy for the candidate types listed in table 9.3, where the first matching possibility is chosen.
This allows for flexible usages of Groovy’s iteration-aware methods. There is no more need to care whether you work with an iterator, an enumeration, a collection, or whatever, for example within a GPath expression.
Supplement:
* Documentation - Operator Overloading
* Documentation - User Guide - Operators
沒有留言:
張貼留言