Proper Properties - A Concrete Proposal for Java 7
Author
Mikael Grev - MiG InfoCom
Uppsala, Sweden
Proposal Version History
0.1 Pre-release
Implementation
Downloads
Change Log
JavaDoc
UML Class Diagram over Property hierarchy
All files are also available from a java.net Subversion repository with the following URL:
https://properties.dev.java.net/svn/properties/trunk
Username and password are same as your java.net user
Discussion and Contact Information
This forum will be used to discuss everything concerning this proposal.
For private messages and discussions you can contact me here: properties (at) miginfocom (dot) com
1. Background
Properties are available in Java 6 and earlier in the form of a naming convention. This can be read in the JavaBean Spec 1.01
. The definition of a property is the implementation of a get[propertyName]/set[propertyName] pair where the logic of the property is the code in these two methods. Which properties a class defines is only denoted by this naming convention. Metadata for using the property in an IDE may optionally be provided for a property using BeanInfos. An enhancement for this metadata is much needed but not provided by this proposal. There are several problems with this naming convention approach, all of which this proposal solves.
- It is hard for a tool to know which methods are real properties and which are not.
- Trivial properties that works like a normal value field are fairly easy to create but properties that supports collections, binding, observability and other features are hard and error prone to write.
- There is no way to convey intention to tools about the value. For instance if it can be null.
- Listening to properties are error prone since the name of the property is derived from a part of the method name, though this is not enforced for listeners, and hence there is no compile time type safety. Using strings is also error prone with refactoring.
- JavaDoc have to be redundantly attached to the getter, setter and field and it is easy get these three targets out of sync.
- It is very hard to create a synchronized property hierarchy where properties and part of other properties. It is especially so for tool interaction.
- Validation of property values are hard to do in combination with other features of a property. There is not standard way to validate a property. What should happen if a validation fails is undefined and without options.
This property proposal provides a concrete solution, including a ready made and fully working implementation, to properties that strengthens the contract between the property writer and the Java framework. The three main goals for Proper Properties are:
- Provide a more clearly defined way to interact with properties of a class/instance.
- Writing properties should be greatly simplified and the quality of them should increase.
- Simplify property hierarchies and make it possible to easily listen to some top property and get notifications when sub properties change, including the property change path.
Note that it is not a primary goal to make it easier to manually set and get values to and from a property. It might be a good target of opportunity though and is therefore added as suggestions at the end of this proposal.
The following objectives are to be fulfilled by this proposal. Properties should (in order of importance)
- be extensible. It should be possible to create different types of properties and reuse them with a single declaration & definition. Every property type that is commonly used today should be possible to model with a property as defined in this proposal.
- be type safe and easily refactorable with the refactoring tools availably at the time of this proposal. This means that property names should be derived from the property field declaration and not needed to be repeated as a string derived from it, other than possibly for interacting with legacy property code.
- support easy and pluggable value validation. The consequence of validation failure should be well defined and extensible.
- support bean binding using the outcome of JSR 295 (Beans Binding). The solution should in general interact well with currently proposed peer JSRs, such as 273, 276, 295, 296, 299, 303, 314 and 318.
- be high performing and with as low memory consumption as possible. The compiled byte code should normally be less for a class containing a number of non-trivial properties than before. The API implementation should be well bounded and not larger than absolutely necessary.
- have extensible out of the box functionality. The normally repeating patterns for properties should be available as configuration of the built in properties. Most properties in a typical class (> 90% ?) should not require a custom implementation. Also, the available options should be easy to use so they encourage developers to create better properties with a more well defined functionality, for instance null handling and collection/map type properties.
- have a simple and easy to understand class hierarchy, yet it should be correct and easy to extend.
- be friendly to language syntax enhancements. Properties should be crafted so that it is possible to enhance the syntax for setting and getting the value as well as a simplifying the declaration initialization of the property itself. It is important that they work in a Java 5 environment.
2. Solution
2.1. Class Hierarchy
You can view an UML diagram over the property hierarchy here.
A concrete class Property defines the general functionality of a property. It has support for observing value changes, validation and provides access to its meta data. Four specific subclasses are provided.
2.1.1. Property Classes and Interfaces
- interface PropertyAdapter - Denotes the minimum required functionality for a property. The intention is that it is only used in adapter type and wrapping contexts. The interface does not enforce a connection to a bean. This interface can be used as a proxy to any property value.
- interface FieldProperty extends PropertyAdapter - Adds mandatory connection to a bean and that the PropertyInto object holds a connection to the Field that holds the property.
- Property implements FieldProperty - The property object to use for properties in a bean container. I holds a single value and supports validation, observability and actual value change detection.
- CollectionProperty extends Property - A collection of values. Has very similar API as CollectionCollection.
- IndexedProperty extends CollectionProperty - Adds indexed gets, sets and adds.
- MapProperty extends Property - A collection of key/value mappings. Has very similar API as
- PropertyBean extends Property - Holds a bean in which there are other properties. It will notify the bean in which it is defined of changes to its properties. This class is thus similar to a "folder" in a tree structure.
2.2. Metadata
Metadata such as if reading and/or writing of the value is supported is provided using a single annotation class, @BeanProperty. It has values that define some functionality of the built in property types. The values of this annotation is only read and stored when the class is first loaded. This information is then accessible through a PropertyInfo instance referenced from the property. This info class contains all information about the property and is only created once for every property (not for every instance of that property).
2.3. Validators
Validators are connected to properties using specific annotations. Which validators are created for a certain property is configurable using validator factories and is thus completely extensible. The most common validators are provided in the base implementation: NullValidator, SizeValidator, RangeValidator and RegexValidator. The Validator contains a single method providing everything needed to validate a property. They are chainable and optionally stateless. They are provided with the context in which to validate, for instance if it is a set, add, put or remove operation. The result of the validation(s) is returned from from the operation (for instance set returns a Result object and property.addAll(Collection) returns a ResultList object). Validators can be dynamically created for any annotation, even without modifying the annotation, through registering ValidatorFactory objects.
2.3. Result and ResultList
The setting of a value on a property always return a Result object denoting the actual result of the operation. Typical results are OK, OK_NO_CHANGE or REFUSE. The Result object optionally contains a status object.
2.4. Binding
Binding properties is not a formal part of this proposal since that is already handled in JSR 295. Proper Properties will work with any kind of binding. This is done through the top most interface PropertyAdapter which supports both stateless and bean connected properties.
3. Examples (Without Language Syntax Enhancements)
3.1. Basic Usage
A simple String property that is initialized to "Joe".
public final Property<String> name = new Property<String>("Joe");
3.2. Validated
A percentage that will not accept a value below 0.0 or above 100.0. Type will be created from the string by using the generic parameter type's constructor that takes a single String argument. Any Comparable object that takes a single String as argument in one of its constructors can be used.
@Range(min = "0.0", max = "100.0")
public final Property<Double> percentage = new Property<Double>(50.0);
An email property string that will always contain a valid email address enforced with a Regular Expression.
@Regex("\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b")
public final Property<String> email = new Property<String>();
or with a custom email validator
@Email
public final Property<String> email = new Property<String>();
3.3. Property Groups
Property groups is used to group properties together so that a listener easily can perform an action for a whole group of properties. For instance the REPAINT action will repaint the bean if it is a Component. The values are Strings, but should normally be pointers to static strings.
@BeanProperty(groups = REPAINT)
public final Property<Paint> background = new Property<Paint>();
@BeanProperty(groups = {INVALIDATE_CACHE, REPAINT})
public final Property<Paint> shadowColor = new Property<Paint>();
3.4. Collection Properties
Collection properties can model any collection of values using any Collection derived class.
The null validator is here configured to not allow a null collection or null values.
The collection is lazily created by default.
@Null(value = false, element = false)
public final CollectionProperty<String> animals = new CollectionProperty<String>();
IndexedProperty is a sub class of CollectionProperty and is used instead of arrays. The advantage is that they are more flexible and can be wrapped in an unmodifiable List instead of being defensively cloned when returned, as is necessary for arrays. The List implementation can be replaced and lazily created.
The @BeanProperty annotation is here specifying that the collection should be wrapped in an unmodifiable collection when returned and setting a new collection is prohibited. The size of the collection can never be zero, it is here validated to always contain at least one element.
@Size(min = 1)
@BeanProperty(get = UNMODIFIABLE, set = NONE)
public final IndexedProperty<String> inMyEars = new IndexedProperty<String>(
{"Tor", "Carl", "Dick", "Joe", "Cote", "Charles"});
MapProperty model a collection of key/value relationships and the property can use any Map derived as the actual storage, wither lazily created by overriding protected T create() or prociding an instance in the constructor.
The null validator is here configured to not allow a null map, null keys or null values.
The property will fire an PropertyEvent if the set value is not the same as the old value (i.e. referential equality). Other possible values are NOT_EQUAL and ALWAYS.
@Null(value = false, mapKey = false, mapValue = false)
@BeanProperty(fire = NOT_SAME)
public final MapProperty<Integer, Decorator> decorators = new MapProperty<Integer, Decorator>(new TreeMap());
3.5. Property Usage
This shows the most common usage patterns for changing the value(s) of a property.
name.set("Dilbert");
myBean.percentage.set(10.0);
myBean.inMyEars.add("Wife", 0)
myBean.decorators.put(10, new MirrorDecorator());
Decorator d = myBean.decorators.get(10);
myBean.activeLevels.add("Donkey");
myBean.activeLevels.addAll(Arrays.asList(1, 2, 3));
3.6. Lazy Init
Property values can be lazily created when first used. This is done by overriding the create method to return the value.
public final IndexedProperty<String> largeLazyArray = new IndexedProperty<String>() {
protected List<T> create() {
return new ArrayList<T>(100000);
}
};
3.7. Result of a Modifying Operation
All modifying calls to the Property always returns a Result object which contains the result of the operation. A ResultList is returned from actions that do multiple operations.
if (email.set("a@b.com").changed())
doExpensiveRepaint();
or for an operation that does multiple operations
.
if (activeLevels.addAll(Arrays.asList(1, 2, 3)).allOk() == false)
notyfyUserOfFailure();
3.8. Observing Properties and Beans
It is possible to listen for single properties as easily as listening for a whole bean. The bean need not have any special methods defined or implement any specific interfaces.
email.addPropertyListener(new PropertyListener<String>() {
public void propertyChange(PropertyEvent<String> e) {
System.out.println("Email Length = " + e.getSource().get().length());
}
});
Listening to all properties of a bean
PropertyHandler.addBeanListener(this, new PropertyListener() {
public void propertyChange(PropertyEvent e) {
System.out.println("Property: " + e.getSource());
}
});
3.9. Property Hierarchies
Properties can be defined in a hierarchy using PropertyBean as a property holding the bean that holds other properties. The great advantage to this is that it is possible to listen for changes on any parent property node and get notifications about sub properties changed. This means that a whole property tree can be built and it is very easy to both keep everything in sync and increase performance by listening only for specific changes to do time consuming updates, for instance.
public final PropertyBean<String> mySubBean = new PropertyBean<String>(new MyBean());
Listening on the class that holds this PropertyBean or directly on that property is now possible. The events are even wrapped so that all events in the chain is retrievable and thus the full property path can be recreated.
4. Proposed Language Enhancements
Even though this property proposal will work in any Java 5.0 environment there are some non-intrusive and easy to contain language syntax enhancements that would greatly simplify properties and increase adoption. They are here listed in order of importance.
4.1. Legacy Interaction
Many smaller things can be done to decrease the transition from old type properties to this new type. It is important that they are mixable in a new environment.
- Add logic to Introspector to make it automatically recognize the new type of properties and provide BeanInfos for them. This would essentially make tools such as IDEs work without modification with proper properties.
- Make the compiler optionally generate getters and setters for the properties (see Unresolved Issues).
- Provide a tool or compiler option that automatically creates inverted wrapping Property objects for all getters and setters if annotated by @BeanProperty. This would make old properties accessible with the new property access syntaxes and thus help migration (see Unresolved Issues).
4.2. Property Bean Initialization
Before any properties in an instance can be used every it must make a call to PropertyHandler.init(this). This should normally be done first in the constructor as it is not required that the bean is properly constructed, only that the class is loaded and properties created.
It should therefore be simple to for any class that contains a Property call this method automatically, just as an empty super constructor is always implicitly called if none is called manually.
4.3. Property Declaration and Definition
Properties of a bean must be declared public final and they must be declared using type parameters (generics) to ensure type safety. The declaration could thus unambiguously be shortened by introducing a context specific property keyword that is 100% backwards compatible. These two declarations/definitions would translate to the exact same byte code:
public final Property<String> name = new Property<String>();
and
property name = new Property<String>();
4.4. Property Value Interaction Enhancement
To increase with adoption, and to make properties easier to use by the manually coding end user, some help should be given to interact with the value of the property. This proposal will intentionally be vague about exactly how this syntax enhancement should be crafted. What's important is that it is done or excluded for good reasons. Here is one idea on how it can look.
Note that the use of a special operator (#, but it could be -> or @ just as well) is used to get the property value instead of the property object as is more commonly proposed. This is to make it clear that the operation might have side effects. Using the normal dot operator to get the value would actually be very bug prone for collection properties since the CollectionProperty API is very similar to the Collection interface.
Using the dot operator will reference the Property object which is exactly as it would pre Java 7, thus enforcing "least surprise".
Value Assignment
myBean#background = Color.WHITE;
is exactly the same, and would produce the same byte code, as
myBean.background.set(Color.WHITE);
Value Retrieval
Color c = myBean#background;
is exactly the same, and would produce the same byte code, as
Color c = myBean.background.get();
4.5. Type Safe Method Pointers
To make listening to properties and beans easier, more light weight and faster way to reference a method would be advantageous to add listeners using method pointers. It is outside the scope of this proposal to fully outline how this should be done but it should be possible to write code like:
email.addPropertyListener(printEmail)
instead of
email.addPropertyListener(new PropertyListener<String>() {
public void propertyChange(PropertyEvent<String> e) {
printEmail(e);
}
});
or for listening to all properties of a bean write
PropertyHandler.addBeanListener(this, myDispatchMethod);
instead of
PropertyHandler.addBeanListener(this, new PropertyListener() {
public void propertyChange(PropertyEvent e) {
myDispatchMethod(e);
}
});
This would lead to cleaner looking code, smaller code bases since less anonymous inner classes need to be created and better performance since less classes will be loaded at startup.
If closures are added to the language and with them every instance isn't creating a new class they would serve the same purpose and this section is void.
5. Unresolved Issues
- Should getters and setters be generated only if some annotation is present? Should that be a separate annotation or a value of @BeanProperty?
- Should there be a suggestion to capitalize the first letter of a Property object? This would help in distinguish them from regular fields and give them an elevated status.
- Which character should be used, if any, to denote the value of a property? #, ->, @ or something else?
- If a property keyword is introduced, as explained in section 4.3, should the normal declaration be allowed as well?
6. Further Refinement and Tasks
6.1. Naming
Some class names and packages will probably need to be changed. This is to be decided upon actual inclusion into the JDK.
6.2. True Statelessness
The properties can be made completely stateless by providing the bean instance to every operation on the property (get, set, add, remove, etc.). This is too cumbersome to do generally though as the syntax for every operation would be:
myBean.set(mybean, value);
instead of the current
myBean.set(value);
To have stateless properties would be very interesting since it means that memory consumption would go down some and that the risk for memory leaks for custom non-field based properties are reduced (not for the provided property implementations in this proposal though as they handle this correctly anyway).
Because of the redundant parameter needed would make the property access harder than the normal setValue(..) syntax it is my belief that this statelessness would reduce the adoption and I am therefore not endorsing it in this proposal. That said I would consider it if a language enhancement provided the parameter automatically in some way for FieldProperty objects and the syntax and interfaces can be refactored to be correct.
7. Proposed IDE Enhancements
A lot can be done in this arena, both with and without language enhancements. Examples will be provided in the next revision of this proposal.
8. JSR Proposal
TBD