Extensible Properties for the JVM, refined
Remi and
Richard are talking about properties again, refining their ideas. I figured I should organize my thoughts as well.
FIrst off, how about some aggressive requirements:
- Properties should support arbitrary implementations! It should be possible to have properties backed by fields, HashMaps, databases, calculations, devices etcetera. Using such a property should require minimal amounts of code.
- Properties should support pluggable behaviours. Properties should be able to be made Observable, non-nullable, validated, copied-on-write, etcetera. Using such a behaviour should require minimal amounts of code.
- Properties should not increase per-instance memory usage.
- Properties should have a minimal impact on performance
- Properties should be toolable.
- Properties should interact cleanly with legacy getters and setters.
Just as the enhanced for loop works using the
Iterable
interface, I think the properties for the JVM should have a
Property
interface like this:
public interface java.lang.Property<B,V> {
void setName(String name);
void setDelegate(Property other);
void set(B bean, V value);
V get(B bean);
}
Note that this interface provides a
delegate to allow
Properties
to be chained. This allows behaviours like observable to be stacked.
When defining a property, the coder provides a list of properties objects to be chained in varargs style:
class Foo {
private property(new ObservableProperty(), new NonNullProperty()) String bar = "phils";
}
...is syntactic sugar for:
class Foo {
private String bar = "phils";
private static Property<Foo,String> $PROPERTY_bar = new ObservableProperty();
static {
Property<Foo,String> p1 = new NonNullProperty();
Property<Foo,String> p2 = new DefaultProperty<Foo,String>() {
public String get(Foo foo) { return bar; }
public String set(Foo foo, String bar) { foo.bar = bar; }
};
p1.setDelegate(p2);
$PROPERTY_bar.setDelegate(p1);
}
}
Let's disect the generated code:
private static Property<Foo,String> $PROPERTY_bar
The property literal is a static member of the class. The property is static so we don't increase memory usage per-instance. The name starts with a $
dollar sign to denote the field is compiler-generated and cannot be referenced normally. = new ObservableProperty()
The property literal instance is the outermost decorated value. This code is the first argument after the property
token in the original source.static { Property<Foo,String> p1 = new NonNullProperty(); p2 = ...
Properties are constructed in order, from outermost to innermost. They're created in a static initializer because the property literals are shared between all instances. = new DefaultProperty{Foo,String}() { ... }
Default code to retrieve the field is generated. Because actual bytecode is created, field access is significantly faster than reflection.p1.setDelegate(p2);
The chain of property literals is linked together, innermost first
It's not as interesting to me, but it's also straightforward to create syntactic sugar for accessing properties:
Foo foo = new Foo();
foo#bar = "bomber";
System.out.println(foo#bar);
... and property literals
Property<Foo,String> barProperty = Foo##bar;
barProperty.set(new Foo(), "revolution");
System.out.println(barProperty.get(new Foo()))
Noting of course that the symbols
#
and
##
are totally arbitrary and could be exchanged with any symbols, such as
:
,
$
,
[]
. I think that multiple symbols are necessary to differentiate between a particular instance's property value and a property literal, otherwise the symbol
#bar
is ambiguous inside the
Foo.java source file.
# posted by Jesse Wilson
on Wednesday, January 24, 2007
2 comments
post a comment
Terminal emulators reviewed
I spend a lot of time at the command line, so a good terminal emulator is important to me. Unfortunately, the ones that get packaged with Linux aren't that great:
KonsoleTerrible text wrapping support, no-reflow and newlines get inserted when you cut & paste
Slow
Gnome Terminal
Bad text wrapping support, no re-flow
Impossible to scroll back while output is being generated
Slow
On the Mac, the built-in terminal works great. But it lacks tabs, so I end up wasting a lot of time in Expose looking for the right window.
Mac OS X Terminal
Really nice split-pane for scrollback
Perfect text wrapping
True transprency
But no tabs!
Mac OS X only!
Fortunately, I've found the perfect terminal emulator. It's fast, it's simple, and it's very easy to install:
Terminator
Unique horizontal scrolling (ideal for SQL)
Fast
Great keyboard shortcuts
Unlimited scrollback is on by default
# posted by Jesse Wilson
on Monday, January 15, 2007
0 comments
post a comment
Fixing IntelliJ crashes in StartupActionScriptManager
On my Ubuntu box
IntelliJ began crashing on startup with an unhelpful stacktrace:
chixdiglinux:intellij-idea-6$ /usr/lib/intellij-idea-5/bin/idea.sh
java.io.EOFException
at java.io.ObjectInputStream$PeekInputStream.readFully(ObjectInputStream.java:2237)
at java.io.ObjectInputStream$BlockDataInputStream.readShort(ObjectInputStream.java:2703)
at java.io.ObjectInputStream.readStreamHeader(ObjectInputStream.java:760)
at java.io.ObjectInputStream.(ObjectInputStream.java:278)
at com.intellij.ide.startup.StartupActionScriptManager.b(StartupActionScriptManager.java:34)
at com.intellij.ide.startup.StartupActionScriptManager.executeActionScript(StartupActionScriptManager.java:2)
at com.intellij.ide.plugins.PluginManager.initClassloader(PluginManager.java:400)
at com.intellij.ide.plugins.PluginManager.bootstrap(PluginManager.java:424)
at com.intellij.ide.plugins.PluginManager.main(PluginManager.java:189)
at com.intellij.ide.plugins.PluginManager.main(PluginManager.java:396)
at com.intellij.idea.Main.main(Main.java:4)
Exception in thread "main" java.lang.AssertionError:
at com.intellij.openapi.diagnostic.DefaultLogger.error(DefaultLogger.java:49)
at com.intellij.openapi.diagnostic.Logger.error(Logger.java:60)
at com.intellij.ide.plugins.PluginManager.initClassloader(PluginManager.java:348)
at com.intellij.ide.plugins.PluginManager.bootstrap(PluginManager.java:424)
at com.intellij.ide.plugins.PluginManager.main(PluginManager.java:189)
at com.intellij.ide.plugins.PluginManager.main(PluginManager.java:396)
at com.intellij.idea.Main.main(Main.java:4)
I tried asking Google, which led me to a
bug that was similar, but not the same. I didn't have the file they instructed for me to delete,
system/plugins/action.script
.
I tried the helpful but awkward tool
lsof
, which tells you which files are opened on your Unix machine. I wanted to get a hint about which file IntelliJ IDEA was crashing on! It turns out that it had a cache in
/var/cache/intellij-idea-5/jessewilson/
, and it was breaking on the
action.script
file in that folder.
Is your IntelliJ crashing on startup? Consider removing the
/var/cache/intellij-idea-5
folder and try again!
# posted by Jesse Wilson
on Monday, January 15, 2007
4 comments
post a comment
EasyMock and OutOfMemoryError: PermGen space
I wrote a test using the easy-to-use
EasyMock Unit-testing API:
package com.publicobject.pizzadelivery;
import junit.framework.TestCase;
import org.easymock.classextension.EasyMock;
public class PizzaDeliveryPersonTest extends TestCase {
private PizzaStore pizzaStore;
private Customer customer;
private Order order;
private Address address;
private DeliveryPerson deliveryPerson;
@Override
public void setUp() {
pizzaStore = EasyMock.createMock(PizzaStore.class);
order = EasyMock.createMock(Order.class);
address = EasyMock.createMock(Address.class);
customer = EasyMock.createMock(Customer.class);
EasyMock.expect(pizzaStore.getNextOrder())
.andReturn(order).anyTimes();
EasyMock.expect(order.getAddress())
.andReturn(address).anyTimes();
EasyMock.expect(order.getPrice())
.andReturn(new Money(17)).anyTimes();
EasyMock.expect(address.getCoordinates())
.andReturn(new GpsCoordinates(30, 40)).anyTimes();
EasyMock.expect(customer.acceptOrder(order))
.andReturn(new Money(20).anyTimes();
EasyMock.replay(pizzaStore);
EasyMock.replay(order);
EasyMock.replay(address);
EasyMock.replay(customer);
}
public void testDeliveryPerson() {
doTestDeliveryPerson(new BasicPizzaDeliveryPerson());
}
public void testDeliveryPersonWithNoGasInCar() {
BasicPizzaDeliveryPerson deliveryPerson = new BasicPizzaDeliveryPerson()
deliveryPerson.getCar().setFuel(0);
doTestDeliveryPerson(deliveryPerson);
}
public void testDeliveryPersonWhoSpeaksFrench() {
BasicPizzaDeliveryPerson deliveryPerson = new BasicPizzaDeliveryPerson()
deliveryPerson.setNativeLanguage(Language.FRENCH);
doTestDeliveryPerson(deliveryPerson);
}
public void testRealTimeDeliveryPerson() {
PizzaDeliveryPerson deliveryPerson = new RealTimePizzaDeliveryPerson()
doTestDeliveryPerson(deliveryPerson);
}
public void testMinimumWageDeliveryPerson() {
BasicPizzaDeliveryPerson deliveryPerson = new BasicPizzaDeliveryPerson()
deliveryPerson.setHourlyWage(new Money(7));
doTestDeliveryPerson(deliveryPerson);
}
public void doTestDeliveryPerson(PizzaDeliveryPerson deliveryPerson) {
assertTrue(deliveryPerson.getTotalMoney().toDollars() == 0);
deliveryPerson.setStore(pizzaStore);
deliveryPerson.runDelivery();
assertTrue(deliveryPerson.getTotalMoney().toDollars() == 20);
assertTrue(deliveryPerson.getTotalTips().toDollars() == 3);
}
}
...but when I ran it in my continuous build, it failed due to a memory leak:
java.lang.OutOfMemoryError: PermGen space
It turns out that every time I call
EasyMock.createMock()
*, a piece of memory is allocated that can
never be reclaimed!My hacky fix was to make all the mocked objects
static
. Now they only get created once, not once for each of the 5 test methods:
public class PizzaDeliveryPersonTest extends TestCase {
private static PizzaStore pizzaStore;
private static Customer customer;
private static Order order;
private static Address address;
private static DeliveryPerson deliveryPerson;
static {
pizzaStore = EasyMock.createMock(PizzaStore.class);
order = EasyMock.createMock(Order.class);
address = EasyMock.createMock(Address.class);
customer = EasyMock.createMock(Customer.class);
EasyMock.expect(pizzaStore.getNextOrder())
.andReturn(order).anyTimes();
EasyMock.expect(order.getAddress())
.andReturn(address).anyTimes();
EasyMock.expect(order.getPrice())
.andReturn(new Money(17)).anyTimes();
EasyMock.expect(address.getCoordinates())
.andReturn(new GpsCoordinates(30, 40)).anyTimes();
EasyMock.expect(customer.acceptOrder(order))
.andReturn(new Money(20).anyTimes();
EasyMock.replay(pizzaStore);
EasyMock.replay(order);
EasyMock.replay(address);
EasyMock.replay(customer);
}
...
}
It gets the job done, but not well. The disadvantages of this approach:
I need to use anyTimes()
on my objects so their methods can be called repeatedly
The code is ugly
I still have a memory leak, it's just a bit smaller
It would be a great fix for EasyMock if multiple calls to EasyMock.createMock(PizzaStore.class)
could share the same generated class object.
# posted by Jesse Wilson
on Wednesday, January 10, 2007
0 comments
post a comment
Extensible Properties for the Java Language
Remi Forax is leading some research on hacking in properties into the JVM. Richard Bair's
latest post has inspired me to write about how I think observable properties could be implemented.
I would like properties to be extensible, so that all the generic boilerplate (not just get/set) can be applied:
public class Person {
private @Property(ObservableProperty.class) String surname;
}
...where ObservableProperty implements a chain-of-command interface for setting properties:
interface Property<B,T> {
/** the delegate holds the 'real' property that we're just decorating */
public void setDelegate(Property<B,T> delegate);
/** @param name the bean property name, such as surname */
public void setName(String name);
public T get(B bean);
public void set(B bean, T value);
}
... and ObservableProperty is implemented something like this:
public class ObservableProperty<B extends Observable,T> implements Property<B,T> {
private Property<B,T> delegate;
private String name;
public void setDelegate(Property<B,T> delegate) {
this.delegate = delegate;
}
public void setName(String name) {
this.name = name;
}
public T get(B bean) {
return delegate.get(bean);
}
public void set(B bean, T value) {
T old = delegate.get(bean);
delegate.set(bean, value);
firePropertyChange(bean.getListeners(), name, old, get(bean));
}
}
The real power of this approach is chaining decorators:
private @Property(ObservableProperty.class, NotNullProperty.class) String surname;
private @Property(ObservableProperty.class, EmailAddressProperty.class) String emailAddress;
private @Property(TransactionalProperty.class) long id;
This approach makes code better because it is
declarative. We are telling the API what to do, not how to do it.
# posted by Jesse Wilson
on Tuesday, January 09, 2007
7 comments
post a comment
Javac bug: the order of imports is significant
The following Java code does something slightly weird - it statically imports a symbol that is defined in the same file. Although it's a little unconventional, it should be perfectly legal:
package com.publicobject.dinosaurs;
import static com.publicobject.dinosaurs.Dinosaur.GeologicPeriod.CRETACEOUS;
import static com.publicobject.dinosaurs.Dinosaur.GeologicPeriod.TRIASSIC;
import static com.publicobject.dinosaurs.Dinosaur.GeologicPeriod.JURASSIC;
import com.publicobject.time.Period;
public class Dinosaur {
private Period period;
private String name;
public static class GeologicPeriod extends Period {
public static final GeologicPeriod TRIASSIC = new GeologicPeriod(-251, -199.6, "Triassic");
public static final GeologicPeriod JURASSIC = new GeologicPeriod(-199.6, -145.5, "Jurassic");
public static final GeologicPeriod CRETACEOUS = new GeologicPeriod(-145.5, -65.5, "Cretaceous");
private String name;
public GeologicPeriod(double startAsMillionYears, double endAsMillionYears, String name) {
super(startAsMillionYears, endAsMillionYears);
this.name = name;
}
}
public Dinosaur(Period period, String name) {
this.period = period;
this.name = name;
}
public boolean isJurassic() {
return period.overlaps(JURASSIC);
}
public boolean isTriassic() {
return period.overlaps(TRIASSIC);
}
public boolean isCretaceous() {
return period.overlaps(CRETACEOUS);
}
}
...except that this Java code does not compile! It fails with a bizarre error, Javac is not able to find the
Period
class:
/Users/jessewilson/Dinosaurs/IntellijProject/source/com/publicobject/dinosaurs/Dinosaur.java:13: cannot find symbol
symbol : class Period
location: class com.publicobject.Dinosaur
public static class GeologicPeriod extends Period {
^
If I rearrange the imports, moving the import of the
Period
class ot the top, the code compiles.
# posted by Jesse Wilson
on Thursday, January 04, 2007
1 comments
post a comment