Summary:
I've implemented what I think is a complete Java implementation of the Scala Option
class. Could you please review and let me know if I have correctly and completely implemented it? And if not, could you please indicate where I have security, multi-threading, persistence, etc. defects?
Details:
I'm learning Scala, but still not able to use it in my projects at work. However, I have fallen in love with Scala's Option
class. It's the equivalent to Haskell's Maybe. It's a great way to consistently implement the "null object" pattern; i.e. type safely getting rid of tons of potentially hidden NullPointerExceptions.
While working on bringing some legacy Java code forward in time (written by me +10 years ago), I kept wanting to use something like Scala's Option
, only written in Java for Java. Thus began my hunt for an effective Java version of Scala's option class.
First, I googled and found something simple from Daniel Spiewak written in 2008: The Option Pattern.
A comment by Tony Morris on Daniel's blog article (above) lead me to an article by Tony Morris where Daniel's idea was made a bit more "complete", also adding to it a bit more complexity (at least to me): Maybe in Java.
Then, while doing further research to try and find something already written, rather than my writing it myself, I found this one. Written earlier this year (2012), it seemed quite up-to-date: Another Scala option in Java.
However, once I brought this latest code into my project to use, it started putting yellow squigglies under any references to None. Basically, the singleton the author used was leaking "raw types" into my code generating warnings. That was particularly annoying as I had just finished eliminating all the raw type code warnings from this particular code base the previous week.
I was now hooked on getting something working that might resemble a work that could/would appear in the high-quality Java libraries, eliminating raw type leakage, adding things like serialization support, etc.
Below is the result as three separate Java class files:
- Option - public interface
- Optional - factory for safely producing instances of Some and None
- Main - JUnit4 test suite to ensure proper functionality
Please review and then give me any feedback and/or corrections you are willing to contribute.
1. Interface Option - public interface
package org.public_domain.option;
import java.util.Iterator;
import java.io.Serializable;
public interface Option<T> extends Iterable<T>, Serializable {
@Override
Iterator<T> iterator();
T get();
T getOrElse(T value);
}
2. Class Optional - Option instance factory
package org.public_domain.option;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
public final class Optional<T> {
@SuppressWarnings("rawtypes")
private static volatile Option NONE_SINGLETON = new OptionImpl();
public static <T> Option<T> getSome(T value) {
if (value == null) {
throw new NullPointerException("value must not be null");
}
return new OptionImpl<T>(value);
}
@SuppressWarnings({"unchecked", "cast"})
public static <T> Option<T> getNone() {
return (Option<T>)getNoneSingleton();
}
@SuppressWarnings("unchecked")
public static <T> Option<T> getOptionWithNullAsNone(T value) {
return
(value != null)
? new OptionImpl<T>(value)
: (Option<T>)getNoneSingleton()
;
}
public static <T> Option<T> getOptionWithNullAsValidValue(T value) {
return new OptionImpl<T>(value);
}
@SuppressWarnings("rawtypes")
static Option getNoneSingleton() {
return NONE_SINGLETON;
}
private Optional() {
//no instances may be created
}
private static final class OptionImpl<T> implements Option<T> {
private static final long serialVersionUID = -5019534835296036482L;
private List<T> values; //contains exactly 0 or 1 value
protected OptionImpl() {
if (getNoneSingleton() != null) {
throw new IllegalStateException("NONE_SINGLETON already defined");
}
this.values = Collections.<T>emptyList();
}
protected OptionImpl(T value) {
List<T> temp = new ArrayList<T>(1);
temp.add(value); //even if it might be a null
this.values = temp;
}
@Override
public int hashCode() {
return this.values.hashCode();
}
@Override
public boolean equals(Object o) {
boolean result = (o == this);
if (!result && (o instanceof OptionImpl<?>)) {
result = this.values.equals(((OptionImpl<?>)o).values);
}
return result;
}
protected Object readResolve() {
Object result = this;
if (this.values.isEmpty()) {
result = getNoneSingleton();
}
return result;
}
@Override
public Iterator<T> iterator() {
return
(!this.values.isEmpty())
? Collections.unmodifiableList(new ArrayList<T>(this.values)).iterator()
: this.values.iterator()
;
}
@Override
public T get() {
if (this.values.isEmpty()) {
throw new UnsupportedOperationException("Invalid to attempt to use get() on None");
}
return this.values.get(0);
}
@Override
public T getOrElse(T valueArg) {
return
(!this.values.isEmpty())
? get()
: valueArg
;
}
}
}
3. Class Main - JUnit4 test suite
package org.public_domain.option.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.junit.Test;
import org.public_domain.option.Option;
import org.public_domain.option.Optional;
public class Main {
@Test
public void simpleUseCasesSome() {
String value = "simpleUseCases";
String valueOther = "simpleUseCases Other";
//Some
Option<String> optionSome = Optional.getSome(value);
assertEquals(value, optionSome.get());
assertEquals(value, optionSome.getOrElse(valueOther));
assertNotSame(valueOther, optionSome.get());
assertNotSame(valueOther, optionSome.getOrElse(valueOther));
//validate simple iterator state (each call is a newly created iterator)
assertTrue(optionSome.iterator().hasNext());
assertEquals(value, optionSome.iterator().next());
assertNotSame(valueOther, optionSome.iterator().next());
//ensure iterator exhausted after single return value (all on the same iterator)
Iterator<String> iterator = optionSome.iterator();
assertTrue(iterator.hasNext());
assertEquals(value, iterator.next());
assertFalse(iterator.hasNext());
}
@Test
public void simpleUseCasesNone() {
String value = "simpleUseCases";
String valueOther = "simpleUseCases Other";
Option<String> optionNone = Optional.getNone();
assertEquals(valueOther, optionNone.getOrElse(valueOther));
assertNotSame(value, optionNone.getOrElse(valueOther));
//ensure iterator is already exhausted
assertFalse(optionNone.iterator().hasNext());
}
@Test (expected=NullPointerException.class)
public void simpleInvalidUseCaseSomePassedNull() {
@SuppressWarnings("unused")
Option<String> optionSome = Optional.getSome(null);
}
@Test (expected=UnsupportedOperationException.class)
public void simpleInvalidUseCaseNoneGet() {
Option<String> optionNone = Optional.getNone();
@SuppressWarnings("unused")
String value = optionNone.get();
}
@Test
public void simpleUseCaseEquals() {
String value = "simpleUseCases";
String valueSame = value;
String valueDifferent = "simpleUseCases Other";
Option<String> optionSome = Optional.getSome(value);
Option<String> optionSomeSame = Optional.getSome(valueSame);
Option<String> optionSomeDifferent = Optional.getSome(valueDifferent);
Option<String> optionNone = Optional.getNone();
Option<String> optionNoneSame = Optional.getNone();
//Some - self-consistency
assertSame(optionSome, optionSome); //identity check
assertEquals(optionSome, optionSomeSame); //content check
assertEquals(optionSomeSame, optionSome); //symmetry check
assertNotSame(optionSome, optionSomeDifferent); //identity and content check
assertNotSame(optionSomeDifferent, optionSome); //symmetry check
//None - self-consistency
assertSame(optionNone, optionNoneSame); //identity check
assertSame(optionNoneSame, optionNone); //symmetry check
//Some-vs-None consistency
assertNotSame(optionSome, optionNone); //identity check
assertNotSame(optionNone, optionSome); //symmetry check
}
@Test
public void useCaseSomeWithNullAsNone() {
String value = null;
String valueSame = value;
String valueDifferent = "simpleUseCases";
Option<String> option = Optional.getOptionWithNullAsNone(value);
Option<String> optionSame = Optional.getOptionWithNullAsNone(valueSame);
Option<String> optionDifferent = Optional.getOptionWithNullAsNone(valueDifferent);
//Some - self-consistency
assertSame(option, option); //identity check
assertEquals(option, optionSame); //content check
assertEquals(optionSame, option); //symmetry check
assertNotSame(option, optionDifferent); //identity and content check
assertNotSame(optionDifferent, option); //symmetry check
//None consistency
Option<String> optionNone = Optional.getNone();
assertSame(option, optionNone);
assertSame(optionNone, option); //symmetry check
}
@Test
public void useCaseSomeWithNullAsValidValue() {
String value = null;
String valueSame = value;
String valueDifferent = "simpleUseCases";
Option<String> option = Optional.getOptionWithNullAsValidValue(value);
Option<String> optionSame = Optional.getOptionWithNullAsValidValue(valueSame);
Option<String> optionDifferent = Optional.getOptionWithNullAsValidValue(valueDifferent);
//Some - self-consistency
assertSame(option, option); //identity check
assertEquals(option, optionSame); //content check
assertEquals(optionSame, option); //symmetry check
assertNotSame(option, optionDifferent); //identity and content check
assertNotSame(optionDifferent, option); //symmetry check
//None consistency
Option<String> optionNone = Optional.getNone();
assertNotSame(option, optionNone);
assertNotSame(optionNone, option); //symmetry check
}
private byte[] transformToByteArray(Object root) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(65536);
try {
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(root);
} catch (IOException e) {
e.printStackTrace();
}
return baos.toByteArray();
}
private Object transformFromByteArray(byte[] content) {
Object result = null;
ByteArrayInputStream bais = new ByteArrayInputStream(content);
ObjectInputStream ois;
try {
ois = new ObjectInputStream(bais);
result = ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return result;
}
@Test
public void useCaseSerialzation() {
String value = "simpleUseCases";
String valueSame = value;
String valueDifferent = "simpleUseCases Other";
Option<String> optionSome = Optional.getSome(value);
Option<String> optionSomeSame = Optional.getSome(valueSame);
Option<String> optionSomeDifferent = Optional.getSome(valueDifferent);
Option<String> optionNone = Optional.getNone();
Option<String> optionNoneSame = Optional.getNone();
Map<String, Option<String>> dataIn = new HashMap<String, Option<String>>();
dataIn.put("optionSome", optionSome);
dataIn.put("optionSomeSame", optionSomeSame);
dataIn.put("optionSomeDifferent", optionSomeDifferent);
dataIn.put("optionNone", optionNone);
dataIn.put("optionNoneSame", optionNoneSame);
byte[] dataInAsByteArray = transformToByteArray(dataIn);
@SuppressWarnings("unchecked")
Map<String, Option<String>> dataOut = (Map<String, Option<String>>)transformFromByteArray(dataInAsByteArray);
assertEquals(optionSome, dataOut.get("optionSome"));
assertEquals(optionSomeSame, dataOut.get("optionSomeSame"));
assertEquals(optionSomeDifferent, dataOut.get("optionSomeDifferent"));
assertSame(optionNone, dataOut.get("optionNone"));
assertSame(optionNoneSame, dataOut.get("optionNoneSame"));
}
}
Option
implementation. \$\endgroup\$Optional<T>
. What we need even more is something similar to Scala'sTry[T]
. \$\endgroup\$