Ruby for the Java world

Script your Java applications and efficiently reuse your Java libraries with this dynamic language

Ever since the invention of the computer, software development has been trending towards higher-level languages. From assembly language, to C, to C++, to Java, each step up has met with the same criticisms from the old guard: it's slow; it's buggy; developers don't want to lose control. But gradually, as hardware has sped up, and new research and development has improved compilers, interpreters, and virtual machines, developers have inevitably migrated to the higher level, enhancing their productivity by freeing themselves from lower-level concerns (see Clayton M. Christensen's Website in Resources for more on this trend).

Java now holds the lead in many areas of software development, but dynamic languages threaten to bypass it in this inexorable climb. Languages such as Python, Perl, Rexx, Groovy, TCL, and Ruby have been doing yeoman service in specialized domains such as file processing, test automation, software builds, glue code, and Web GUIs for years—hence, their historic name "scripting languages." But in the last few years, they have been making headway in the heavy-duty jobs once reserved mostly for C++, Java, and other compiled languages.

In the last year, the Ruby on Rails (RoR) Web framework has given Ruby a big boost. RoR builds definitions for all tiers of a typical Web application—GUI, business logic, and persistence—from simple Ruby code, thereby minimizing redundancy, boilerplate code, source-code generation, and configuration. RoR's ease of use showcases the Ruby language; and Ruby, a full-fledged software language, has much more to offer than RoR.

As a long-time Java developer, I am likely to stick with Java for some time to come. But I keep my eye on other languages that can play a role in my Java-based systems, and Ruby has recently emerged as a particularly good candidate. With the help of the JRuby interpreter, Ruby works well with Java, configuring, integrating, and reusing Java software (more on that below). And simply learning Ruby has improved my Java code. Ruby lets me easily accomplish techniques such as functional programming and metaprogramming, which in Java I can only do with difficulty. Learning these techniques in Ruby has helped me better appreciate when and how to use them in Java development.

With this article, I hope to share with you some of my excitement about what Ruby can do for my Java-based systems. I compare the strengths and weaknesses of both Java and Ruby, and present the pros and cons of the JRuby interpreter. I also show where to best draw the dividing line between Java and Ruby to benefit from each. I illustrate these points with code samples and present a messaging example that shows how to integrate Java systems with Ruby, putting to good use the flexibility, expressiveness, and power of a dynamic metaprogrammable language.

Ruby vs. Java

This article explains Ruby from the Java developer's viewpoint, focusing on a comparison between the two languages. Like Java, Ruby is a full-featured object-oriented language. But there are many significant differences. Ruby is dynamically typed and runs in a source-code interpreter, and it conveniently supports metaprogramming as well as the procedural and functional paradigms. I won't go into details of Ruby syntax, since that subject has been covered extensively elsewhere. (For tutorials and documentation, see the Ruby homepage in Resources.)

Dynamic typing

Java has static typing. You declare the type of each variable, and then, during compilation, you get an error message if you use a variable of the wrong type. Ruby, on the other hand, has dynamic typing: You don't declare types for variables or functions, and no type-check occurs until runtime, when you get an error if you call a method that doesn't exist. Even then, Ruby doesn't care about an object's class, just whether it has a method of the name used in the method call. For this reason, the dynamic approach has earned the name duck typing: "If it walks like a duck and quacks like a duck, it's a duck."

Listing 1. Duck typing

 

class ADuck def quack() puts "quack A"; end end

class BDuck def quack() puts "quack B"; end end

# quack_it doesn't care about the type of the argument duck, as long # as it has a method called quack. Classes A and B have no # inheritance relationship. def quack_it(duck) duck.quack end

a = ADuck.new b = BDuck.new quack_it(a) quack_it(b)

Java also lets you achieve dynamic typing, using reflection, but this clumsy and verbose workaround produces confusing exceptions like NoSuchMethodError and InvocationTargetException; in practice, these exceptions tend to pop up in reflective Java code far more often than the equivalents in Ruby.

Even in nonreflective Java code, you often lose static type information. For example, execute() methods in the Command design pattern (see "Java Tip 68: Learn How to Implement the Command Pattern in Java") must return Object rather than a specific type in pre-Java 5 code, resulting in ClassCastExceptions. Likewise, when signatures change between compile-time and runtime, runtime Errors ensue. In practice, whether in Java or Ruby, such errors rarely cause severe field bugs. A strong unit test suite—which you need anyway!—generally catches them in time.

Ruby's dynamic typing means you don't repeat yourself: How often in Java have you had to suffer through verbose code along the lines of XMLPersistence xmlPersistence = (XMLPersistence)persistenceManager.getPersistence();? Ruby eliminates the need for the type declaration and casting (as well as parentheses and semicolon): a typical Ruby equivalent would be xmlPersistence = persistence_manager.persistence.

Ruby's dynamic typing does not mean weak typing—Ruby always requires you to pass objects of the correct type. Java, in fact, enforces types more weakly than Ruby. For example, Java evaluates "4" + 2 as "42", coercing the integer to a string, while Ruby throws a TypeError, telling you it "can't convert Fixnum into String." Likewise, Java, sacrificing correctness for speed, can silently overflow an integer operation, producing weirdness such as Integer.MAX_VALUE + 1, which equals Integer.MIN_VALUE, while Ruby simply expands integers as needed.

Despite Ruby's advantages, Java's static typing does give it one ability that leaves it as the preferred choice for large-scale projects: Java tools understand code at development-time. IDEs can trace dependencies between classes, find usages of methods and classes, auto-complete identifiers, and help you refactor code. Though parallel Ruby tools exist with limited functionality, they lack type information and so cannot perform all these tasks.

Interpreted language

Ruby runs in an interpreter, so you can test code without a distracting wait from the compiler; you can even run Ruby interactively, executing each line as you type it. Besides the fast feedback, interpreted languages have a rarely noted advantage in dealing with field bugs. With compiled languages, to analyze the code causing a field bug, the field engineer must determine the deployed application's exact build version and then look up the code in the source-code configuration management system. In real life, often this is simply impossible. With interpreted languages, on the other hand, the source code is immediately available for analysis and even, when necessary, for emergency on-the-spot fixes.

Interpreted languages have an unfortunate reputation for slowness. Compare the history of Java, a "semi-compiled" language: In the early years, the JVM always interpreted the bytecode at runtime, which contributed to Java's reputation for slowness. Yet Java developers quickly learned that most applications spend most of their time waiting on the user or network I/O and the remaining bottlenecks are best optimized in higher-level algorithms, rather than in raw low-level tweaks. Over the years, Java rapidly gained speed; for example, just-in-time compilers with optimization based on dynamic analysis sometimes allow Java to outdo C++, which is limited to optimization based on static compile-time analysis.

Already, for most applications, Ruby is no slower than other languages. In the near future, Ruby will get a further boost as Ruby's native interpreter moves to a bytecode-based system and the JVM JRuby interpreter gains the ability to compile Ruby to Java bytecode. Eventually, any performance lag will gradually become negligible for most functionality.

Functional programming

Ruby supports multiple programming paradigms with equal ease. In addition to its pure object-oriented style (even integers and the like are objects), it also supports a procedural style suited to one-off scripts: you can write code outside any class or function, as well as functions outside any class. (Ruby silently adds these functions to the Object class, maintaining the object-orientation behind the scenes.)

Most interestingly, for Java programmers looking for new and useful perspectives, Ruby supports the functional paradigm. You can use most functional programming constructs in Java, with some support from libraries such as Jakarta Commons Collections, but the syntax is clumsier. (See "Functional Programming in the Java Language" by Abhijit Belapurkar and Jakarta Commons Collections in Resources.) Ruby, though not a purely functional language, treats functions and anonymous blocks of code as full citizens of the language, which can be passed around and manipulated like any ordinary objects.

In Java, you often iterate over a collection's Iterator, running the logic on each element. But the iteration is just an implementation detail: in general, you are just trying to apply the same logic to each element of a collection. In Ruby, you pass a code block to a method that does just that, iterating behind the scenes. For example, [1, 2, 3, 4, 5].each{|n| print n*n, " "} prints the string 1 4 9 16 25; an iterator takes each element of the list and passes it into the code block as the variable n.

Functional programming is useful in wrapping code blocks with instructions to be executed before and after the block. For example, in Java, you can use the Command design pattern to ensure the opening and closing of a file, database transaction, or other resource. This burdens the code with the useless overhead of an anonymous inner class and callback method; in addition, there is the distracting rule that variables passed into the anonymous Command class must be declared final. And to ensure that logic always executes at the end of a code block, the whole thing must be wrapped with try{...}finally{...}.

In Ruby, you can wrap any function or code block, with no need for anonymous classes or method definitions. In Listing 2, the file opens, then closes after the write() method. No code is needed beyond the transaction() method and the code block.

Listing 2. Wrap a code block

 File.open("out.txt", "a") {|f|
    f.write("Hello")
}

Metaprogrammable language

You generally define your Java classes as source code, but you can also manipulate class definitions at runtime. This requires advanced techniques such as bytecode enhancement during class-loading. Hibernate, for example, inserts data-access logic directly into the business objects' bytecode, saving the application programmer from coding the extra data-access layer. But class manipulation, called metaprogramming, is practical only for infrastructure programmers: application programmers cannot usefully introduce these tricky and fragile techniques.

Even at development time, Java limits the developers' ability to change classes. To add an isBlank() method that tells you if a String is all white space, you'd have to add a StringUtils class with the static method (as Apache Commons does); logically, the new method belongs in String.

In Ruby, on the other hand, you can simply extend the built-in String class with a blank? method. In fact, because in Ruby everything is an object, you could even augment the Fixnum class, the equivalent of Java's primitive int, as shown in Listing 3.

Listing 3. Add method to built-in classes String and Fixnum

 

class String # Returns true if string is all white space. # The question mark indicates a Boolean return value. def blank?() !(self =~ /\S/) end end

class Fixnum # Returns 0 or 1 which in Ruby are treated as false and true respectively. def odd?() return self % 2 end end

puts " ".blank? # true # The next line evaluates if-then similarly to Java's ternary operator ?: puts (if 23.odd? then "23 odd" else "23 even" end)

1 2 Page
Recommended
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more