Learn more about groovy and scala classes in Java

Time:2019-12-8

Preface

Java inherits the platform, not the language. There are more than 200 languages that can run on a JVM, and inevitably one of them will eventually replace the Java language as the best way to write a JVM program. This series will explore three next-generation JVM languages: groovy, Scala and clojure, and compare and contrast new features and examples to give Java developers a general understanding of their future development in the near future.

Java developers are proficient in C + + and other languages, including multiple inheritance, so that classes can inherit from any number of parent classes. One problem with multiple inheritance is that it is impossible to determine which parent class the inherited functionality comes from. This problem is called the diamond problem (see resources). Diamond problem and other complexity inherent in multi inheritance inspire Java language designers to choose the method of “single inheritance plus interface”.

The interface defines semantics, but no behavior. They are ideal for defining method signatures and data abstractions, and all Java next-generation languages support Java interfaces without major changes. However, some cross cutting problems are not suitable for the “single inheritance plus interface” model. This mismatch leads to the need to provide external mechanisms suitable for the Java language, such as aspect oriented programming. Two Java next-generation languages (groovy and scala) deal with this problem at another level of extension by using a language structure called blending or feature. This article introduced the features of mixing in groovy and Scala, and demonstrated how to use them. (clojure handles roughly the same functionality through protocols, which I covered in part 2 of Java’s next generation: no inherited extensions.)

Mix in

The concept of blending originated in the flavors language (see resources). The idea came from an ice cream shop near the office where the language was developed. This ice cream shop offers pure flavors of ice cream, as well as any other “mix” customers want (candy chips, sugar chips, nuts, etc.).

Some early object-oriented languages defined the attributes and methods of a class together in a single code block, and all the class definitions were complete. In other languages, developers can define properties in one place, but defer the definition of methods and “mix” them into classes when appropriate. With the evolution of object-oriented language, the details of the way to mix with modern language are also evolving.

In ruby, groovy and similar languages, as a cross between an interface and a parent class, blending can extend the existing class hierarchy. Just like an interface, blending can act as the type of instanceof checking, while following the same extension rules. You can apply an unlimited number of blends to a class. Unlike the interface, blending not only specifies method signature, but also implements signature behavior.

In the first language that includes blending, blending contains only methods, not States, such as member variables. Many languages (including groovy) now include stateful blending. Scala features also operate in a stateful way.

Groovy’s mix in

Groovy implements blending through the metaclass. Mixin() method or the @ mixin annotation. (@ mixin annotation uses groovy abstract syntax tree (AST) transformation in turn to support the required metaprogramming pipeline.) The example in Listing 1 uses metaclass. Mixin() to enable the file class to create zip compressed files:

Listing 1. Mixing the zip () method into the file class


class Zipper {
def zip(dest) {
new ZipOutputStream(new FileOutputStream(dest))
.withStream { ZipOutputStream zos ->
eachFileRecurse { f ->
if (!f.isDirectory()) {
zos.putNextEntry(new ZipEntry(f.getPath()))
new FileInputStream(f).withStream { s ->
zos << s
zos.closeEntry()
}
}
}
}
}
static {
File.metaClass.mixin(Zipper)
}
}

In Listing 1, I create a zipper class that contains the new zip () method and the connection to add the method to the existing file class. The (trivial) groovy code of the zip () method recursively creates a zip file. The last part of the listing adds the new method to the existing file class by using a static initializer. In the Java language, the static initializer of a class runs when the class is loaded. Static initializers are an ideal place to augment code, because you should make sure to run the initializer before you run any code that depends on the enhancement. In Listing 1, the mixin () method adds the zip () method to the file.

In “extensions without inheritance, Part 1,” I introduced two groovy mechanisms: expandometaclass and class classes, which you can use to add, change, or delete methods on existing classes. The final result of adding methods with mixin () is the same as that of adding methods with expandometaclass or class class class, but their implementation is different. Consider the example of mixing in in Listing 2:

Listing 2. Mix in manipulation inheritance hierarchy


import groovy.transform.ToString
class DebugInfo {
def getWhoAmI() {
println "${this.class} <- ${super.class.name} 
<<-- ${this.getClass().getSuperclass().name}"
}
}
@ToString class Person {
def name, age
}
@ToString class Employee extends Person {
def id, role
}
@ToString class Manager extends Employee {
def suiteNo
}
Person.mixin(DebugInfo)
def p = new Person(name:"Pete", age:33)
def e = new Employee(name:"Fred", age:25, id:"FRE", role:"Manager")
def m = new Manager(name:"Burns", id:"001", suiteNo:"1A")
p.whoAmI
e.whoAmI
m.whoAmI

In Listing 2, I create a class called debuginfo that contains a getwhoami property definition. In this attribute, I print out some details of the class, such as the current class and parent-child relationship description of super and getclass(). Getsuperclass(). Next, I create a simple class hierarchy that includes person, employee, and manager.

Then I mix the debuginfo class with the person class that resides at the top of the hierarchy. Because person has the whoamI attribute, its subclass also has that attribute.

In the output, you can see (and may be surprised) that the debuginfo class inserts itself into the inheritance hierarchy:


class Person <- DebugInfo <<-- java.lang.Object
class Employee <- DebugInfo <<-- Person
class Manager <- DebugInfo <<-- Employee

Blending methods must adapt to the existing complex relationships in groovy for method parsing. The different return values of the parent class in Listing 2 reflect these relationships. The details of method analysis are beyond the scope of this paper. But be careful with the dependencies on this and super values (and their various forms) in the blend method.

Using a class class or expandometaclass does not affect inheritance, because you are only modifying the class, not mixing in different new behaviors. One disadvantage of this is that you cannot recognize these changes as a different category of artifacts. If I use a class class or expandometaclass to add the same three methods to more than one class, then no specific code component (such as interface or class signature) can identify the existing commonality. The advantage of blending is that groovy treats everything that uses blending as a category.

One of the troubles with class class implementation is the strict class structure. You must fully use static methods, each of which takes at least one parameter to represent the type being extended. Metaprogramming is the most useful way to eliminate such boilerplate code. @The presence of mixin annotations makes it easier to create categories and mix them into classes. Listing 3 (from the groovy documentation) illustrates the synergy between categories and mashups:

Listing 3. Combining categories and blending


interface Vehicle {
String getName()
}
@Category(Vehicle) class Flying {
def fly() { "I'm the ${name} and I fly!"}
}
@Category(Vehicle) class Diving {
def dive() { "I'm the ${name} and I dive!"}
}
@Mixin([Diving, Flying])
class JamesBondVehicle implements Vehicle {
String getName() { "James Bond's vehicle" }
}
assert new JamesBondVehicle().fly() ==
"I'm the James Bond's vehicle and I fly!"
assert new JamesBondVehicle().dive() ==
"I'm the James Bond's vehicle and I dive!"

In Listing 3, I create a simple vehicle interface and two class classes (flying and diving). @The category annotation focuses on the requirements of the boilerplate code. After defining the categories, I mixed them into a James Bond vehicle to connect the two behaviors.

The intersection of classes, expandometaclass and blending in groovy is the inevitable result of positive language evolution. There are obvious overlaps among the three technologies, but each technology has its own strengths that can be best handled. If you redesign groovy from scratch, the author might integrate multiple features of the three technologies into one mechanism.

Features of Scala

Scala implements code reuse through features, which is a core language feature similar to blending. Features in scala are stateful (they can include both methods and fields), and they play the same instanceof role as interfaces in the Java language. Features and blending solve many of the same problems, but features gain more support in language rigor.

In “common in groovy, Scala, and clojure, Part 1,” I used a complex class to illustrate operator overloading in scala. I didn’t implement the Boolean comparison operator in this class because Scala’s built-in ordered feature makes the implementation trivial. Listing 4 shows an improved complex class that takes advantage of the ordered feature:

Listing 4. Comparing plurals


final class Complex(val real:Int, val imaginary:Int) extends Ordered[Complex] {
require (real != 0 || imaginary != 0)
def +(operand:Complex) =
new Complex(real + operand.real, imaginary + operand.imaginary)
def +(operand:Int) =
new Complex(real + operand, imaginary)
def -(operand:Complex) =
new Complex(real - operand.real, imaginary - operand.imaginary)
def -(operand:Int) =
new Complex(real - operand, imaginary)
def *(operand:Complex) =
new Complex(real * operand.real - imaginary * operand.imaginary,
real * operand.imaginary + imaginary * operand.real)
override def toString() =
real + (if (imaginary < 0) "" else "+") + imaginary + "i"
override def equals(that:Any) = that match {
case other :Complex => (real == other.real) && (imaginary == other.imaginary)
case _ => false
}
override def hashCode():Int =
41 * ((41 + real) + imaginary)
def compare(that:Complex) :Int = {
def myMagnitude = Math.sqrt(this.real ^ 2 + this.imaginary ^ 2)
def thatMagnitude = Math.sqrt(that.real ^ 2 + that.imaginary ^ 2)
(myMagnitude - thatMagnitude).round.toInt
}
}

I did not implement >, <, = and > = operators in Listing 4, but I can call them in plural instances, as shown in Listing 5:

Listing 5. Test comparison


class ComplexTest extends FunSuite {
test("comparison") {
assert(new Complex(1, 2) >= new Complex(3, 4))
assert(new Complex(1, 1) < new Complex(2,2))
assert(new Complex(-10, -10) > new Complex(1, 1))
assert(new Complex(1, 2) >= new Complex(1, 2))
assert(new Complex(1, 2) <= new Complex(1, 2))
}
}

Because you don’t need to use mathematically defined techniques to compare complex numbers, in Listing 4, I use an algorithm that is generally accepted to compare the size of numbers. I use the ordered [complex] feature to extend the class definition, which blends in the Boolean operators of the parameterized class. For the feature to work, the injected operator must compare two complex numbers, which is the purpose of the compare () method. If you try the extended ordered feature but do not provide the required methods, the compiler message informs you that your class must be declared abstract because the required methods are missing.

In Scala, features have two clearly defined roles: enriching interfaces and performing stackable modifications.

Rich interface

When designing an interface, Java developers are faced with a problem that depends on convenience: should we create a rich interface with many methods, or a thin interface with only a few methods? The rich interface is more convenient for its consumers because it provides a wide palette of methods, but the absolute number of methods makes the interface more difficult to implement. The problem with thin interfaces is the opposite.

Feature can solve the dilemma of using rich interface or thin interface. You can create core functionality in a thin interface and then extend it with features to provide richer functionality. For example, in Scala, set feature implements a set sharing function, and the child feature you select (mutable or immutable) has determined whether the setting is variable or not.

Stackable modifications

Another common use of features in scala is stackable modifications. With features, you can modify existing methods and add new ones, and super provides access to implementations that can link back to previous features.

Listing 6 illustrates the stackable changes with some queues:

Listing 6. Building stackable changes


abstract class IntQueue {
def get():Int
def put(x:Int)
}
import scala.collection.mutable.ArrayBuffer
class BasicIntQueue extends IntQueue {
private val buf = new ArrayBuffer[Int]
def get() = buf.remove(0)
def put(x:Int) { buf += x }
}
trait Squaring extends IntQueue {
abstract override def put(x:Int) { super.put(x * x) }
}

In Listing 6, I create a simple intqueue class. Then I build a variable version that includes arraybuffer. The squaring feature extends all intqueues and automatically square values when they are inserted into the queue. Calls to super within the squaring feature provide access to the previous feature in the stack. In addition to the first method, as long as each overridden method calls super, the modification stack will be stacked one by one, as shown in Listing 7:

Listing 7. Example of building a stack


object Test {
def main(args:Array[String]) {
val queue = (new BasicIntQueue with Squaring)
queue.put(10)
queue.put(20)
println(queue.get()) // 100
println(queue.get()) // 400
}
}

The use of super in Listing 6 illustrates the important difference between features and blending. Because you have (and do) blend in the original classes after they have been created, blending must address potential uncertainties in the current location in the class hierarchy. Features are linearized when creating classes; the compiler solves the problem of what a super is without any uncertainty. Strictly defined complex rules (which are beyond the scope of this article) control how linearization works in scala. Features also solve the diamond problem for Scala. When Scala tracks the source and resolution of methods, uncertainty is not possible because the language defines clear rules to handle resolution.

Concluding remarks

In this installment, I’ve explored the similarities and differences between mashups (in groovy) and features (in scala). Blending and features provide many similar features, but they differ in the details of implementation. In some important aspects, they illustrate different philosophy of language. In groovy, blending exists in the form of comments and uses the powerful metaprogramming capabilities provided by ast transformations. There is a slight (but important) difference between the mix, class, and expandometaclass in terms of functionality. Features in Scala, such as ordered, form the core language features that most of scala’s built-in functionality relies on.

The above is the whole content of this article. I hope it will help you in your study, and I hope you can support developepaer more.