Nice is an object-oriented, JRE-compatible programming language that has been formulated with an emphasis on modularity, expressiveness, and safety. Unlike the Java language, which is purely object oriented, Nice incorporates functional and agile development techniques, including some of those found in aspect-oriented programming. Like many of the newer development languages, Nice has the advantage of building on the shortcomings of its predecessors, including the Java language. What's more, Nice offers many of the same features found in Java 1.5, but makes them available on any JVM.
In this fourth installment of the alt.lang.jre column, I'll introduce you to the most interesting and useful features of Nice, including parametric classes, design-by-contract constructs, multimethods, and much, much more.
In the Java language, the central unit of functionality is embodied in classes. Nice, however, moves this notion up one level to packages. Nice
lets you define multiple classes, interfaces, and methods in one file
ending with a .nice postfix. Once a file has been
so defined, it typically becomes a package. Nice's compiler,
nicec, is used to compile .nice files and is available as both an Ant task and an
Eclipse plug-in (see Resources).
Running a Nice application requires the definition of a main() method. You define this method just as you would
a main() method in the Java language. But Nice's main method differs from the Java language method in that it is defined in a package, not a class. This method can therefore become a static hook to jumpstart a Nice application, much like a static main() method in a Java class allows you to run a class from the command line.
You can see how Nice's main() method works in Listing 1, which is a simple application that verifies some basic math knowledge. Notice that Nice supports the assert keyword, even though I'm running on a pre-1.4 VM.
Listing 1. A simple Nice program
void main(String[] args){
assert 4 + 4 == 8;
}
|
In Nice, as you'll see shortly, methods can be defined outside of classes. The implications of defining methods at a package level will become clearer as you read.
Code safety is one of the most powerful features Nice can bring to your
development toolbox. Nice is particularly effective when it comes to dealing with two very common Java exceptions: the underhanded ClassCastException and NullPointerException.
These two exceptions have posed a longtime challenge for developers working
on the Java platform. Java 5.0 incorporates generic types as an effective means of addressing ClassCastExceptions, but this
adaptation will do little for many corporations still using Java 1.3. Nice, on the other hand, was developed with ClassCastExceptions and NullPointerExceptions in mind. As such, the language
supports two features, parametric classes and optional types,
that go a long way toward keeping applications from throwing these exceptions. What's more, with Nice you can employ these features today, on any
Java platform 1.2 or higher.
In the Java language, all collections hold the least-common-denominator type, which is Object. As a result, the Java developer must cast types every time he or she retrieves an element, which can be a burden in larger programs. To demonstrate the problem, I'll show you how the Java language deals with types, and then how Nice's parametric classes simplify the issue.
Listing 2 shows an IStack interface, representing a stack data structure defined in the Java language.
Listing 2. The IStack interface defined in the Java language
public interface IStack {
int size();
boolean isEmpty();
Object pop();
Object peek();
void push(Object obj);
}
|
This Java implementation of IStack is quite
simple, but still works as a stack data structure, as shown in
Listing 3.
Listing 3. The IStack Implementation
import java.util.ArrayList;
import java.util.List;
public class SimpleStack implements IStack {
private List items;
public SimpleStack(){
this.items = new ArrayList();
}
public int size(){
return this.items.size();
}
public boolean isEmpty(){
return this.items.isEmpty();
}
public Object pop(){
return this.items.remove(this.items.size() - 1);
}
public Object peek(){
return this.items.get(this.items.size() - 1);
}
public void push(Object obj){
this.items.add(obj);
}
}
|
While fairly effective, this stack implementation requires tedious type casts whenever I utilize it, as demonstrated in Listing 4.
Listing 4. JUnit example showing tedious type casts
import junit.framework.TestCase;
import org.age.nice.examples.stack.IStack;
import org.age.nice.examples.stack.SimpleStack;
public class SimpleStackUseTest extends TestCase {
private IStack stack;
public void testSimpleIntegerPops(){
this.stack.push(new Integer(1));
this.stack.push(new Integer(2));
Integer two = (Integer)this.stack.pop();
TestCase.assertEquals("value should be 2", 2,
two.intValue());
}
public void testSimpleIntegerPopsWillNotCompile(){
this.stack.push(new Integer(1));
this.stack.push(new Integer(2));
//Integer two = this.stack.pop();
//TestCase.assertEquals("value should be 2", 2, two.intValue());
}
public void testPopClassCastException(){
this.stack.push(new Integer(1));
this.stack.push(new Double(2.0));
try{
Integer two = (Integer)this.stack.pop();
TestCase.fail("top item was successfully cast to Integer!");
}catch(ClassCastException e){
//ignore expected behavior
}
}
protected void setUp() throws Exception {
this.stack = new SimpleStack();
}
}
|
In Listing 4, the testSimpleIntegerPops()
method shows how you must cast the result of the pop operation to an Integer. If you fail to perform a cast, you produce a
ClassCastException, as demonstrated in the
testPopClassCastException() method. Look closely at
the commented out lines of the testSimpleIntegerPopsWillNotCompile method. They won't compile with normal Java code; but doesn't that code look nice?
Parametric classes, or templates, if you come from a C++ background, are commonly used to simplify collections handling, among other functions. Parametric classes let you define a single, precise type that the collection can then hold.
To define a generic Stack implementation in Nice, you use the <Type> syntax when defining the class, as well as for desired parameters and return types. In Listing 5, you can see how you could redefine and simplify your stack data structure with a SimpleStack class using Nice's <Type> syntax.
Listing 5. A Nice parametric stack
class SimpleStack<Type> {
List<Type> items = new ArrayList();
void push(Type t) {
items.add(t);
}
Type pop(){
return items.removeAt(items.size() - 1);
}
Type peek(){
return items.get(items.size() - 1);
}
boolean isEmpty(){
return items.isEmpty();
}
int size(){
return items.size();
}
}
|
The Nice parameterized SimpleStack class
is now defined to hold specific types, defined at compile time,
not at runtime as in the Java language. Notice how in Listing 5 both the pop() and peek() methods
return Type, and how the push() method takes Type as a parameter. This Type matches the object type that the internal items collection will hold.
Listing 6 shows how a parametric stack is used in Nice. Note that I've created a SimpleStack instance that can hold only objects of type String. Notice how the last line, which attempts to push an Integer onto the parameterized stack, won't compile
in Nice!
Listing 6. Using a parametric stack in Nice
void main(String[] args){
SimpleStack<String> stk = new SimpleStack();
stk.push("Groovy");
stk.push("Ruby");
assert stk.pop() == "Ruby";
assert stk.peek() == "Groovy";
//following line won't compile!
//stk.push(new Integer(1));
}
|
The NullPointerException is probably the
most familiar exception for any Java developer. Indeed, null pointers
are so nefarious that even the simplest Java compilers can flag objects
as not having been initialized. Unfortunately, you may have
discovered at one time or another, those warnings do not catch every
possible occurrence of null.
In keeping with its touchstone property of safety, Nice provides the
notion of optional types. Because optional types are defined by the
author of the API in the Java language, they are often difficult to determine,
so Nice prefixes them with a question mark (?).
Optional types are particularly useful if you've ever found yourself
developing to an API that takes numerous parameters. You've most likely
determined that various parameters are optional either because the JavaDoc comments specify them as such or because
you've passed in null and noted that things
appear to work.
With Nice, you could simply add a ? before
a parameter to signify that the variable is capable of being
null; conversely, variables without the
question mark cannot be explicitly set to null. You can see how this works in Listing 7,
where I've defined a method, walk(), in the
Dog class. This signifies that the second
parameter of type Location is capable of
being null.
Listing 7. Defining an optional type in Nice
class Leash{
int length;
}
class Location{
String place;
}
class Dog{
String name;
void walk(Leash leash, ?Location location){
var place = (location == null)?
" no where " : " to " location.place;
println("walking " name "
with a leash " leash.length " inches long"
//won't compile-> location.place
place);
}
}
|
Notice how in Listing 7, the walk() method
must now account for the fact that location can be null. In
the println() method, the compiler will not
allow the code to actually reference location.place.
Listing 8 offers a demonstration of the effectiveness of optional
types, in this case using the walk() method defined in Listing 7. Notice that you can now legally pass in null and you can also legally pass in a valid value for location.
Listing 8. Demonstration of optional types in Nice
Leash lsh = new Leash(length:35); Dog mollie = new Dog(name:"Mollie"); mollie.walk(lsh, null); Location loc = new Location(place:"The Coffee Shop"); mollie.walk(leash:lsh, location:loc); |
Do not confuse the null capability syntax with optional parameters, which are quite different.
As you probably noticed in Listing 8, Nice allows you to specify
parameters when invoking a method. When I invoked the walk() method on the Dog
instance, I explicitly named the parameters. For example, I set the
lsh variable of type Leash to the first parameter, leash.
Nice also lets you specify parameters in a constructor, much like you
can in languages such as Groovy and Jython. In Listing 8, when I
created new instances of Dog and Leash, I set each instance's properties, name, and
length respectively, explicitly in the constructors.
Naming parameters allows you to pass them in any desired order. In Listing 9, for example, both calls are fundamentally the same; because I've named the parameters it doesn't matter what order I pass them in.
Listing 9. Further demonstration of optional types in Nice
mollie.walk(leash:lsh, location:loc); //same behavior from walk method mollie.walk(location:loc, leash:lsh); |
Optional parameters are even more useful than optional types. They can actually work in place of optional types. Listing 10 shows how I could
redefine the walk() method using optional parameters.
Listing 10. Optional parameters in Nice
void walkAgain(Leash leash, Location location=new Location(place:"nowhere")){
println("walking (again) " name " with a leash " leash.length
" inches long to " location.place);
}
|
This code defines the second parameter, location, as being optional. When
the method is invoked, this parameter does not have to be passed in. If
no value is passed in, the default value will be used. In this case, the default
value is a Location with a place equal to nowhere.
Optional parameters free you of the need to program defensively, as
i did in Listing 7. Unlike the walk() method in Listing 7, the walkAgain() method of Listing 10 need
not be written around the possibility of encountering null values.
The final thing to note about optional parameters is that they can be overridden with other values. As Listing 11 shows, I am able to call walkAgain with a choice of one or two parameters.
Listing 11. Overriding optional parameters in Nice
mollie.walkAgain(lsh); Location locBY = new Location(place:"the backyard"); mollie.walkAgain(lsh, locBY); |
Design by contract (DBC) is a technique for ensuring that all the components in a system do what they're meant to do, by explicitly stating each component's intended functionality and expectations of the client in its interface. Eiffel (see Resources) is a popular language utilizing DBC. Its techniques have been incorporated by many languages, including Java 1.4, which introduced the use of assertions. Nice uses the keywords requires and ensures to incorporate contractual information in your programs and assertions to validate the state of that programs during execution. Additionally, Nice supports the assert keyword even for pre-1.4 JVMs.
The requires and ensures keywords function like preconditions and postconditions, allowing you to define the conditions
that desired methods will act upon and guarantee. An unmet condition will
result in the generation of assertion exceptions at runtime. The combination of conditions and assertions has saved many an application
from sinking into the deep hole that can be caused by logical errors. I'll show you how these two mechanisms work together in the examples that follow.
The requires clause states the
requirements that must be met by the client of a method. If the
requirements are not met, the method will be abandoned and an assertion
exception generated. The ensures clause works
on the side of a method's client. This clause is the guarantee the
method commits to its associated caller.
Listing 12 defines a CoffeeMachine class that contains a brew() method. In the definition of brew(), I've stipulated that clients must
pass in a Coffee instance with a beanAge property less than 10. Otherwise, an
assertion exception will be generated with the clause "Beans are too
old to brew." Moreover, I've also guaranteed that the result of the method, in this case an instance of
CoffeeCup, will have a temp property greater than 155.
Listing 12. DBC ensures an ideal brew
class CoffeeMachine{
CoffeeCup brew(Coffee cfe)
requires cfe.beanAge < 10 : "Beans are too old to brew"
ensures result.temp > 155 : "Coffee isn't hot enough to serve" {
return new CoffeeCup(coffee: cfe, temp:160, isFull:true);
}
}
class Coffee{
int beanAge;
}
class CoffeeCup{
int temp;
boolean isFull;
Coffee coffee;
}
|
Listing 13 shows a new CoffeeMachine instance along with a new instance of
Coffee. Notice that in this case I've set the Coffee property of beanAge to 15, which is, of course, greater than 10
and will consequently not meet brew's
contract.
Listing 13. A violation of contract
CoffeeMachine machine = new CoffeeMachine(); Coffee cfe = new Coffee(beanAge:15); CoffeeCup cup = machine.brew(cfe); |
Listing 14 demonstrates the exception stack that is generated when
brew() is called in Listing 13. As you can
see, the customized message "Beans are too old to brew" is present to
facilitate debugging.
Listing 14. These beans are too old to brew!
Exception in thread "main" nice.lang.AssertionFailed: Beans are too old to brew at test.dispatch.brew(MoreCoffee.nice:6) at test.fun.main(HelloWorld.nice:111) at test.dispatch.main(MoreCoffee.nice:0) |
Nice assertions are not enabled by default, so they must be explicitly turned on. See Nice's documentation for directions specific to your JVM.
With a basic understanding of how Nice implements preconditions and
postconditions, let's look at what happens when I apply these techniques to
the earlier IStack example. In Listing 15, I've defined an IStack interface in Nice and
added various ensures and requires clauses.
Listing 15. The IStack interface with conditions added
interface IStack<T>{
int size() ensures result >= 0 : "size can not be less than one";
void push(T t) ensures size(this) > 0 :
"pushing an item should increase the size";
boolean isEmpty() ensures result == (size(this) == 0) :
"if size is zero, result should be false";
T pop() requires !isEmpty(this) : "Can not pop an empty stack";
T peek() requires !isEmpty(this) : "Can not pop an empty stack";
}
|
In Listing 16, I've implemented the IStack interface. Notice that Nice gives you a shortcut for defining method bodies: you simply use the = syntax. Also note the use of Nice's override syntax in the example below.
Listing 16. The new and improved stack
class DBCStack<T> implements IStack{
ArrayList<T> contents = new ArrayList();
override void push(T t) = contents.add(t);
override T peek() = contents.get(contents.size() - 1);
override T pop() = contents.removeAt(contents.size() - 1);
override boolean isEmpty() = contents.size() == 0;
override int size() = contents.size();
}
|
For the most part I can use the new DBCStack the same as before. If I attempt
to violate the terms of IStack's contract, however,
it'll cause assertion exceptions. In Listing 17, for example, you can see what happens when I attempt to push a third item on a stack that has been ensured for only two items. The third pop() invocation causes the precondition, requires !isEmpty(this), to
fail. Consequently, an AssertionFailed
exception is generated with the custom message: "cannot pop an empty
stack."
Listing 17. Testing the limits of the new stack
let IStack<Dog> dbcStack = new DBCStack();
dbcStack.push(new Dog(name:"Stella"));
dbcStack.push(new Dog(name:"Mollie"));
println(dbcStack.pop().name);//mollie
println(dbcStack.pop().name);//stella
// throws assertion error -> println(dbcStack.pop().name);
// Exception in thread "main" nice.lang.AssertionFailed:
// Can not pop an empty stack
// Can not pop an empty stack
/ at test.dispatch.pop(StackImpl.nice:10)
// at test.fun.main(HelloWorld.nice:129)
// at test.dispatch.main(StackImpl.nice:0)
|
One of the most interesting and unique features of Nice is multimethods, or the ability to define a class instance method outside of a particular class's definition. This feature alone creates an abundance of extensiveness comparable to some of the more exciting tenets of aspect oriented programming (AOP).
A multimethod's syntax is quite easy, because the first parameter is the
type to which the method should be affixed. Remaining
parameters become the standard parameters to the instance method. To
illustrate, in Listing 18, I can create a simple, meaningless method
and attach it to instances of java.lang.String.
Listing 18. Multimethods in Nice
void laugh(String str){
println("haha, I'm holding an instance of " str);
}
|
In this code, the laugh() method simply
prints a String when invoked. Because the first
parameter to the laugh() method is of type
String, the method is therefore attached to
instances of String. In Listing 19, I create
a String instance, myString, and invoke the laugh() method, which prints "haha, I'm holding an
instance of Andy."
Listing 19. Using Multimethods in Nice
let myString = new String("Andy");
myString.laugh();
|
While the laugh() method is completely useless, it does illustrate a few key points:
- Nice lets you easily affix new behavior to objects.
- Nice allows you to attach this behavior to anything, including standard classes whose source code you cannot access.
- Nice lets you add behavior to
finalobjects.
Adding useful behavior to objects is more fun than toying with useless ones, so let's take our knowledge of parametric classes and multimethods to the next level. In Listing 20, you'll see what happens when I add a join() method to
the java.util.Collection interface.
The join() method is
quite common among agile languages; it simply appends a desired String to all elements in a collection, thus
creating a large String.
Listing 20. Adding a join method to the Collections interface
/**
* multi method, adds a join call to a collection.
* @return a string like 1-2-3-4.
*/
<T> String join (Collection<T> collection, String value = " "){
StringBuffer buff = new StringBuffer();
let size = collection.size();
var x = 0;
for (T elem : collection){
buff.append(elem);
if(++x < size){
buff.append(value);
}
}
return buff.toString();
}
|
Listing 20 demonstrates how you can use Nice's multimethods functionality to attach a join() method to any typed Collection. In this case, the
join method has an optional parameter: the String with which you will join the elements of the Collection instance on which the method is called.
Listing 21 shows how effortless it is to use the new join() method. I simply pass in the
desired join String or use the default. This functionality is just like static crosscutting in
AOP, but the Nice version is arguably much easier!
Listing 21. A nice new join method!
Collection<int> nColl = new ArrayList();
nColl.add(1);
nColl.add(3);
nColl.add(3);
println(nColl.join("**")); //prints 1**3**3
println(nColl.join()); //prints 1 3 3
|
In addition to multimethods, Nice offers a second means to affix additional behaviors to objects. Abstract interfaces are similar to normal Java interfaces, but far more flexible. The nicest thing about abstract interfaces is that they can be implemented by any object, even after they've been defined. In this regard, abstract interfaces function much like static crosscutting in AOP.
In Listing 22, you can begin to get an idea of how abstract interfaces
work. I start by creating a new abstract
interface of type TasteTest with one method,
taste(). I then proceed to make the Mocha and Latte classes
implement this new type.
Listing 22. Abstract interfaces in Nice
package test;
class Latte {
getPrice() = new BigDecimal(2.50);
}
class Mocha {
getPrice() = new BigDecimal(4.30);
}
abstract interface TasteTest{
void taste();
}
class test.Mocha implements TasteTest;
class test.Latte implements TasteTest;
taste(test.Mocha mcha) = println("Ohh this is good...");
taste(Latte lte) = println("Waking me up it's soooo good.");
|
Using the new functionality on instances of Latte is quite simple, as shown in Listing 23. I simply call
the taste method!
Listing 23. Latte wake up call
let coffee = new Latte(); coffee.taste(); //prints Waking me up it's soooo good. |
While the examples using multimethods and abstract interfaces are necessarily quite brief, they do demonstrate the level of expressiveness that can be achieved using Nice. In fact, some would argue that Nice's expressiveness, bolstered by the simplicity of its syntax, rivals that of AOP in Java programming.
As previously noted, Nice incorporates some features found in Java 5.0, but enables you to use them now on virtually any Java platform. One of these features is enumerated types. Like parametric classes, enumerated types can assist in bug detection at compile time, rather than at runtime.
To understand how enumerated types work, I'll use a common
development example. Constants or keys are commonly placed in interfaces and
classes as static final fields. Other classes
then reference these fields, rather than local variables, in an
attempt to limit variation. We've all seen code that defines
constants like those found in Listing 24.
Listing 24. Example Java constants
public class CoffeeBeans {
public static final int ESPRESSOROAST = 1;
public static final int KONA = 2;
public static final int FRENCHROAST = 3;
public static final int MOCHA = 4;
}
|
Listing 25 shows a typical example of constant types in action. If you study the code carefully, you'll also see where their effectiveness is compromised.
Listing 25. The limits of Java constants
public static void brew(int coffeeType){
if(coffeeType == CoffeeBeans.ESPRESSOROAST){
System.out.println("brewing espresso!");
}
//other if/else clauses....
}
|
The problem with the code in Listing 25 is that villainous or just
plain ignorant clients can call the brew()
method with values not within the defined bounds. For example, if I
call the method with 48, the code will
compile perfectly. Unfortunately, I'll still have to deal with the
defect when I run the code.
Using enumerations rather than constants would make this code
safer. Enumerations would force the compiler to, in essence, guarantee
values within defined bounds. For example, in Listing 26, you can
see what happens when I define an enumeration for CoffeeBeanTypes, as well as defining an interface
of type ICoffee and two implementations:
Latte and Mocha.
As you can see, these ICoffee types define a
getType() method that returns an instance of
the enumeration. You can also see what happens if I define a CoffeeMachine class with a brew() method that takes an instance of the
enumeration, rather than an int as shown in
Listing 25.
Listing 26. Defining enumerations in Nice
enum CoffeeBeanType(String value){
ESPRESSOROAST("espresso"),
KONA("kona"),
FRENCHROAST("French Roast"),
MOCHA("Mocha Java")
}
interface ICoffee{
CoffeeBeanType getType();
BigDecimal getPrice();
}
class Latte implements ICoffee{
getType()= ESPRESSOROAST;
getPrice() = new BigDecimal(2.50);
}
class Mocha implements ICoffee{
getType()= FRENCHROAST;
getPrice() = new BigDecimal(4.30);
}
class CoffeeMachine{
}
void brew(CoffeeMachine machine, CoffeeBeanType type){
println("Brewing a coffee with " type.value " beans" );
}
|
In Listing 27, I use the enumerated type when calling the brew() method on the instance of CoffeeMachine. Notice how enumerations in Nice
implicitly contain a value field. In the case
of ESPRESSOROAST, the value is the String that was passed in on creation: espresso.
Listing 27. Using enumerations in Nice
let cfe = new CoffeeMachine();
cfe.brew(ESPRESSOROAST);
//prints Brewing a coffee with espresso beans
let coffee = new Latte();
println("\n My latte cost me $" coffee.getPrice()
" and is brewed with " coffee.getType().value " beans");
|
Nice is a strongly typed language that eschews the notion of casting objects, so all collections in Nice must be parameterized. For example, the line below will compile normally in the Java language, but not in Nice, where the collection must be more strongly typed.
Collection noColl = new ArrayList(); //will not compile in Nice |
If for some reason you need to render a Nice collection more Java-like, you can simply parameterize it with java.lang.Object, as shown below.
Collection<Object> collObj = new ArrayList(); |
Like some other languages, Nice has added a host of additional methods onto the standard collections, using multimethods. Nice's collection behaviors are similar to those found in Groovy and Ruby in that they support a block-like syntax. While these code blocks are really just anonymous methods and not nearly as powerful as true closures, they're still quite handy.
Nice provides a slick iterator-like method on collections aptly named
foreach(), as shown in Listing 28. Notice how
the foreach() method takes a block of code
signified by the =>. In this case, the block
simply prints the incoming value of i.
Listing 28. The foreach method on collections
Collection<Integer> coll = new ArrayList();
coll.add(new Integer(1));
coll.add(new Integer(2));
coll.add(new Integer(3));
coll.foreach(Integer i => {
println(i);
}); //prints 1 2 3
|
Nice has also enhanced the Java language's Set interface with a handful of powerful methods, as demonstrated in Listing 29.
Listing 29. Powerful Set methods in Nice
Set<int> testSet = new HashSet(); Set<int> otherSet = new HashSet(); testSet.add(1); testSet.add(2); testSet.add(3); otherSet.add(3); otherSet.add(4); otherSet.add(5); Set<int> nSet = testSet.intersection(otherSet); nSet.foreach(int i => println(i)); //prints 3 Set<int> uSet = testSet.union(otherSet); uSet.foreach(int i => println(i)); //prints 1,2,3,4,5 Set<int> difSet = testSet.difference(otherSet); difSet.foreach(int i => println(i)); //prints 1,2 Set<int> dSet = testSet.disjunction(otherSet); dSet.foreach(int i => println(i)); //prints 1,2,4,5 |
As you can see in the code above, Nice provides an intersection() method that finds common elements in two separate Sets. The union()
method combines two Sets, while the difference() method finds the difference between a
pair of Sets. Lastly, the disjunction() method combines two Sets, dropping common elements.
I'll close this introduction to Nice with a look at three of its more interesting convenience features: value dispatching, enhanced
for loops and ranges, and more relaxed String usage. As in the previous sections, you'll notice that each of these features brings greater expressiveness and modularity to your code while also enhancing its safety.
Value dispatching methods are used to ensure that the runtime decision
of which method to actually call or dispatch to is not only determined
by a parameter's type, but by its actual value. Such methods can
help you avoid code that contains a series of if/else clauses or switch statements.
You can see how this works in Listing 30. I start by defining an enum for music
genres. I then create a series of value dispatch methods which, in
effect, simulate a switch statement. If I then
pass in a Genre of value Celtic, the variationName() method will return "Irish," as shown
below.
Listing 30. Value dispatching defined Nicely
enum Genre(String value) {
Celtic("Celtic"), Rock("Rock"), Folk("Folk"),
Jazz("Jazz")
}
String variationName(Genre gre);
variationName(gre) = "No variations Available";
variationName(Celtic) = "Irish";
variationName(Folk) = "Acoustic";
variationName(Rock) = "Pop";
variationName(Jazz) = "Smooth Jazz";
|
As shown in Listing 31 below, when I pass in the Genre enum of value Folk, the resulting variation variable is set to Acoustic.
Listing 31. Using value dispatching in Nice
var variation = variationName(Folk); println(variation); //prints Acoustic |
Enhanced for looping and ranges
You've probably already noticed by now that Nice supports a shorthand
notion of the standard for loop, much like
the new Java 5. A simple for loop
construct is such a common facet among agile languages that it's a
wonder the Java language has taken so long to introduce it!
As shown in Listing 32, Nice lets you easily iterate over a collection of ints without having to use the Java language's normal Iterator interface. Also note that Nice supports
autoboxing, much like Groovy (see Resources for the alt.lang.jre installment on Groovy).
Listing 32. A Nice for loop
Collection<int> iColl = new ArrayList();
iColl.add(11);
iColl.add(12);
for(int i : iColl){
println(i);
}
|
Listing 33 demonstrates range functionality in Nice. Nice supports inclusive ranges only; consequently, in the code bellow, the numbers 1 through (and including) 20 are printed.
Listing 33. Inclusive range in Nice
for(int i : 1..20){
print(i);
}
|
Nice provides normal Java Strings, but relaxes some of the constraints associated with their usage in the Java language. If you've worked with Python, you'll recognize Nice's multiline string literals. In Listing 34, you see how easy it is to use the """ syntax to create mutliline strings. Notice, also, how a ' is automatically escaped.
Listing 34. Multiline strings in Nice
var poem = """
This is a multiline String.
Why wouldn't anyone want to make one?
""";
println(poem);
var line1 = "All roads lead to where you are";
var line2 = "Love don't need to find a way";
var concat = "Da da " line1 " da da " line2;
println(concat);
println("concat " line1 " with " line2);
|
Nice also makes string concatenation a breeze by relaxing the Java language's normal + syntax. As a result, you can call println() and drop the associated concatenations, as shown
above in Listing 34.
Many of the nicest features demonstrated in this month's installment of alt.lang.jre are also available in the 5.0 release of the Java language. Not everyone can immediately jump on the Java 5 bandwagon, however, and for the rest of us, there's Nice. In addition to letting you play with parametric classes, multimethods, design by contract, and numerous other convenience features on practically any version of the JVM, Nice gives you a gentle introduction to the benefits of expressiveness and agility on any development platform. As demonstrated here, Nice is a particularly sound choice when it comes to crafting safer, more modular code on the Java platform. See the Resources section to learn more about Nice, and stay tuned for next month's installment of alt.lang.jre, which will provide an introduction to Rhino.
- Visit the Nice home page on SourceForge to learn more about Nice's many excellent features.
- Don't miss a single installment in the new alt.lang.jre series!
Visit the alt.lang.jre series page for a complete listing of all columns.
- Abhijit Belapurkar's "Functional programming in the Java language" (developerWorks, July 2004) explains some of the differences between functional and object-oriented languages and shows you how to write more functional Java code.
- Python is one of the languages that influenced the design of Nice. Build on what you've learned here and learn more about functional programming at
the same time, with David Mertz's two-part introduction to "Functional programming in Python" (developerWorks, March 2001), part of his regular Charming Python series.
- AOP techniques such as static crosscutting and joins were briefly
discussed in this column. Learn more about AOP with Andrew Glover's
"AOP banishes the tight-coupling blues" (developerWorks, February 2004).
- Find out how AOP techniques can improve the modularity of your code,
with Nicholas Lesiecki's "Improve modularity with aspect-oriented programming" (developerWorks, January 2002).
- Filippo Diotalevi's "Contract enforcement with AOP" (developerWorks, July 2004) will introduce you to the principals of design by contract on the Java platform.
- Visit the Eiffel homepage to learn more about design by contract on Eiffel.
- Find hundreds of articles about every aspect of Java programming in the
developerWorks Java technology zone.
- Browse for books on these and other technical topics.

Andrew Glover is the President of Stelligent Incorporated, a Washington, DC, metro area company specializing in the construction of automated testing frameworks, which lower software bug counts, reduce integration and testing times, and improve overall code stability.
Comments (Undergoing maintenance)





