Skip to content

Instantly share code, notes, and snippets.

@mtorchiano
Last active April 3, 2017 09:28
Show Gist options
  • Save mtorchiano/9305ea9cd74dee31a47797bdd10037d8 to your computer and use it in GitHub Desktop.
Save mtorchiano/9305ea9cd74dee31a47797bdd10037d8 to your computer and use it in GitHub Desktop.
Java idioms for exposing properties

Java idioms for exposing properties

Creative Commons License
This work by Marco Torchiano is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.


In several application where a general purpose container needs to access specific objects, properties represent a common technique.

We can loosely define a property as an attribute of an object that can be accessed programmatically based on run-time only information. That is the accessor -- typically developed in advance and independently -- does not know anything of the accessed object structure at compile-time.

There are few Java-specific idioms that can be used to implement properties: they use different technologies, some present since the first versions of Java, some introduced more recently; they require different efforts on developer side, and they sport different time performance levels.

Alternative solutions

There are several possible solutions available in Java to let a class expose properties:

  • implement a well known interface
  • provide methods adhering to a predefined naming scheme
  • use annotations

For the sake of simplicity here I will focus on a very simple set of properties that can be either String or int. They can be accessed through a predefined interface:

public interface Gettable {
	public int getInt(String name) throws PropertyNotFound;
	public String getString(String name) throws PropertyNotFound;
}

In order to understant better how the different alternatives work we consider a simple class with two properties:

  • a String property with name "s"
  • an int property with name "i"

Implement interface

The first approach requires the class to implement the Gettatble interface and provide the methods that map a property name to its value, this can be done using series of if, a switch (that since Java 7 accepts also String selectors), or using a Map of command objects.

/*
 * Class implementing the Gettable interface
 */
class GettableClass implements Gettable {
	private int i;
	private String s;
	public GettableClass(int i, String s) { this.i=i; this.s=s; }
	@Override
	public int getInt(String name) throws PropertyNotFound {
		if(name.equals("i")) return i;
		throw new PropertyNotFound(name);
	}
	@Override
	public String getString(String name) throws PropertyNotFound{
		if(name.equals("s")) return s;
		throw new PropertyNotFound(name);
	}
}

When a class implements the predefined interface its properties can accessed directly:

GettableClass go = new GettableClass(1,"value");
\\...
go.getString("s");

Methods adhering naming scheme

According to the second technique, the class provides a set of getter methods that conform to a predefined naming scheme: one method for each property. The method must start with get followed by the name of the property with initial letter capitalized, e.g. for a property named Name a method getName() must be created.

Such an approach is widely used, e.g. it is the scheme used in the Java Beans naming convention.

/*
 * Java Bean convention for getters
 */
class BeanClass {
	private int i;
	private String s;
	public BeanClass(int i, String s) { this.i=i; this.s=s; }
	public int getI()  { return i; }
	public String getS() {return s; }
}

When a class provides methods according to such a predefined naming scheme, its properties can accessed through an adapter object that makes use of reflection to access the variables:

BeanAdapter rao = new BeanAdapter(new BeanClass(1,"value"));
\\ ...
rao.getString("s");

Using annotations

The final approach consists of a class with only the attributes related to the properties, and possibly a constructor. The attributes defined in the class defines are marked with Java Annotations that indicate that attributes are to be exposed as properties.

In this case the @Property annotator is used to indicate which attributes are considered as attributes.

/*
 * Annotated class
 */
static class AnnotatedClass {
	@Property  int i;
	@Property("s")  String string;
	public AnnotatedClass(int i, String s) { this.i=i; this.string=s; }
}

When a class uses annotations its properties can accessed through an adapter object that can be automatically generated at compile-time or can make use of reflection to access the variables:

AnnotationAdapter aao = new AnnotationAdapter(new AnnotatedClass(1,"value"));
\\...
aao.getString("s");

Another possibility for annotatote class is to leverage and annotation processo that can be automatically generate a custom adapter at compile-time:

AnnotatedClassPropAdapt caao = new AnnotatedClassPropAdapt(new AnnotatedClass(1,"value"));
\\...
caao.getString("s");

Summary

The complexity of the declarations and the relative performance are summarized in the following table:

Declaration method LOC Required Methods Access method Access time
Implement interface 13 2 Direct 520 ms
Method naming scheme 7 2 ( one per property ) Run-time reflection adapter 828 ms
Annotations 5 0 Run-time reflection adapter 720 ms
" " " Compile-time adapter 402 ms

Each declaration method require a specific access method. The annotation approach allows for two alternative access mehtods.

The times refer to 10M executions of the following block:

String s = obj.getString("s");
int j = obj.getInt("i");
s += j;

The time were measured on a MacBook Pro (Retina, 15-inch, Late 2013) with a 2.3 GHz Intel Core i7 CPU.

Implementation

Direct interface implementation

The easiest solution consists in simply implementing a predefined interface (e.g. Gettable in the example above) that can be invoked by any accessor object to access the properties. The interface might include meta-property methods, e.g. to retrieve the list of properties.

This solution is the most expensive in terms of developer's effort because it requires the most code to be written. In addition -- and partly as a consequence -- it is the most difficult to maintain.

This solution does not require any additional code because the object properties can be accessed directly.

Method naming scheme

This method is widely used e.g. in the Java Beans naming convention where each accessor method must start with get. Another usage of this approach (though not for property access) can be found in the JUnit 3 method naming scheme where each test method has to start with test and return void.

This solution is easier to implement, although it might turn expensive when the number of properties grows. The solution requires an adapter that implements the predefined interface and converts any general interface method invocation into a target-specific method invocation.

The properties can be accesse by means of Java Reflection API that enable a program to programmatically access class definitions.

To access a String property we can on-the-fly retrieve a method corresponding to the naming schema and invoke it:

String getStringPropertyBean(Object obj, String name) throws PropertyNotFound {
	String getterName = "get"+Character.toUpperCase(name.charAt(0))+(name.length()==1?"":name.substring(1));
	try{
		Method m = obj.getClass().getMethod(getterName);
		return (String) m.invoke(obj);
	}catch(Exception e){
		throw new PropertyNotFound(name);
	}
}

The above code, can be highly inefficient if the property is accessed several times, because the search for a method might be time-consuming. For this reason it is preferrable to have an adapter object that on-instantion scans the target property container object and stores the pointers for the right invocation. A simplified implementation is reported in the BeanAdapter.

Annotations

The usage of annotations is the most compact and readable solution. The solution requires an adapter that implements the predefined interface and converts any general interface method invocation into an access to the target attribute.

Annotations are used to attach additional meta-information to the elements of the program. Such elements may be retained up to the run-time phase according to the declaration provided in the definition of the annotation.

The annotations can be analyzed with the Java Reflection API, similarly to the naming scheme approach. To access a String property we can on-the-fly identify the attribute annotated as property and access them (this requires proper visibility or disabling the visibility access using the method Field.setAccessible()):

public static String getStringPropertyAnnotated(Object obj, String name)throws PropertyNotFound {
	for(Field f : obj.getClass().getDeclaredFields()){
		Property prop = f.getAnnotation(Property.class);
		if(prop!=null){
			String propName = (prop.value().equals("")?f.getName():prop.value());
			if(propName.equals(name)) {
				try {
					f.setAccessible(true);
					String result = (String) f.get(obj);
					f.setAccessible(false);
					return result;
				} catch (IllegalArgumentException | IllegalAccessException e) {
					break;
				}
			}
		}
	}
	throw new PropertyNotFound(name);
}

As for naming scheme, the on-the-fly approach is not efficient and an adapter object that analyzes the target object and prepare a data structure with the pointers to the attributes. AnnotationAdapter.

As an alternative to run-time access, annotations can be processed at compile-time by suitable annotation processors. An annotation processor is invoked directly by the compiler and it can generate additional code that is compiled. An example of annotation processor is represented byt the PropertyProcessor class.

The PropertyProcessor annotation processor, applied to the AnnotatedClass shown above, generates the following adapter class:

import it.polito.po.annotations.PropertyNotFound;
import it.polito.po.annotations.Gettable;
import it.polito.prove.BeansTiming.AnnotatedClass;

public class AnnotatedClassPropAdapt implements Gettable {
  private AnnotatedClass target;
  public AnnotatedClassPropAdapt(AnnotatedClass target){
    this.target=target;
  }
  public String getString( String name ) throws PropertyNotFound {
    if(name.equals("s")){
      return target.string;
    }
    throw new PropertyNotFound(name);
  }

  public int getInt( String name ) throws PropertyNotFound {
    if(name.equals("i")){
      return target.i;
    }
    throw new PropertyNotFound(name);
  }
}

The class is very similar to the original GettableClass, in particular it is automatically placed in the same package as the target class in order to be able to access the attributes that have a package visibility.

class AnnotationAdapter implements Gettable {
private interface Getter { String get(); }
private interface IntGetter { int get(); }
private HashMap<String,Getter> getters=new HashMap<>();
private HashMap<String,IntGetter> intGetters=new HashMap<>();
public AnnotationAdapter(Object target){
for(Field f : target.getClass().getDeclaredFields()){
Property prop = f.getAnnotation(Property.class);
if(prop!=null){
String propName = (prop.value().equals("")?f.getName():prop.value());
if(f.getType().getName().equals("java.lang.String")){
getters.put(propName, () -> {
try {
//f.setAccessible(true);
String result = (String) f.get(target);
//f.setAccessible(false);
return result;
} catch (IllegalArgumentException | IllegalAccessException e) {
}
return "";
});
}else{
intGetters.put(propName, () -> {
try {
int result = (Integer)f.get(target);
return result;
} catch (IllegalArgumentException | IllegalAccessException e) {
}
return -1;
});
}
}
}
}
public String getString( String name ) throws PropertyNotFound {
Getter g = getters.get(name);
if(g==null) throw new PropertyNotFound(name);
return g.get();
}
public int getInt( String name ) throws PropertyNotFound {
IntGetter g = intGetters.get(name);
if(g==null) throw new PropertyNotFound(name);
return g.get();
}
}
class BeanAdapter implements Gettable {
private interface Getter { String get(); }
private interface IntGetter { int get(); }
private HashMap<String,Getter> getters=new HashMap<>();
private HashMap<String,IntGetter> intGetters=new HashMap<>();
public BeanAdapter(Object target){
for(Method m : target.getClass().getDeclaredMethods()){
String name = m.getName();
if(name.startsWith("get")){
String propName = Character.toLowerCase(name.charAt(3))+(name.length()==4?"":name.substring(4));
if(m.getReturnType().getName().equals("java.lang.String")){
getters.put(propName, () -> {
try {
String result = (String) m.invoke(target);
return result;
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
return "";
}
});
}else{
intGetters.put(propName, () -> {
try {
int result = (Integer) m.invoke(target);
return result;
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
return -1;
}
});
}
}
}
}
@Override
public String getString( String name ) throws PropertyNotFound {
Getter g = getters.get(name);
if(g==null) throw new PropertyNotFound(name);
return g.get();
}
@Override
public int getInt( String name ) throws PropertyNotFound {
IntGetter g = intGetters.get(name);
if(g==null) throw new PropertyNotFound(name);
return g.get();
}
}
package it.polito.po.annotations;
public interface Gettable {
public int getInt(String name) throws PropertyNotFound;
public String getString(String name) throws PropertyNotFound;
}
package it.polito.po.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) \\ can be applied to class fields only
@Retention(RetentionPolicy.RUNTIME) \\ must be retained until run-time
public @interface Property {
String value() default "";
}
package it.polito.po.annotations;
import java.awt.datatransfer.FlavorListener;
import java.io.BufferedWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import it.polito.po.annotations.Property;
@SupportedAnnotationTypes("it.polito.po.annotations.Property")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class PropertyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "INVOKED");
HashMap<TypeElement,List<VariableElement>> classes = new HashMap<>();
for (Element elem : roundEnv.getElementsAnnotatedWith(Property.class)) {
Property prop = elem.getAnnotation(Property.class);
TypeElement declClass = (TypeElement)elem.getEnclosingElement();
classes.computeIfAbsent(declClass, e -> new LinkedList<VariableElement>());
classes.get(declClass).add((VariableElement)elem);
}
for(TypeElement cls : classes.keySet()){
Element pack = cls.getEnclosingElement();
while(pack.getKind()!=ElementKind.PACKAGE){
pack = pack.getEnclosingElement();
}
PackageElement declPackage = (PackageElement)pack;
try {
final String targetClass = cls.getSimpleName().toString();
final String newClass = cls.getSimpleName() + "PropAdapt";
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(declPackage.getQualifiedName()+"."+newClass);
BufferedWriter out = new BufferedWriter(jfo.openWriter());
out.append("package ").append(declPackage.getQualifiedName()).append(";\n\n");
out.write("import it.polito.po.annotations.PropertyNotFound;\n\n");
out.write("import it.polito.po.annotations.Gettable;\n\n");
out.append("import ").append(cls.getQualifiedName()).append(";\n\n");
out.append("public class ").append(newClass).append(" implements Gettable {\n");
out.append(" private ").append(targetClass).append(" target;\n");
out.append(" public ").append(newClass).append("(").append(targetClass).
append(" target){\n this.target=target;\n }\n");
out.append(" public String getString( String name ) throws PropertyNotFound {\n");
for(VariableElement fld : classes.get(cls)){
if(fld.asType().toString().equals("java.lang.String")){
Property prop = fld.getAnnotation(Property.class);
String propName = (prop.value().equals("")?fld.getSimpleName().toString():prop.value());
out.append(" if(name.equals(\"").append(propName).append("\")){\n")
.append(" return target.").append(fld.getSimpleName()).append(";\n }\n");
}
}
out.write(" throw new PropertyNotFound(name);\n }\n\n");
out.append(" public int getInt( String name ) throws PropertyNotFound {\n");
for(VariableElement fld : classes.get(cls)){
if(fld.asType().toString().equals("int")){
Property prop = fld.getAnnotation(Property.class);
String propName = (prop.value().equals("")?fld.getSimpleName().toString():prop.value());
out.append(" if(name.equals(\"").append(propName).append("\")){\n")
.append(" return target.").append(fld.getSimpleName()).append(";\n }\n");
}
}
out.write(" throw new PropertyNotFound(name);\n }\n");
out.append("}\n");
out.close();
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.getMessage());
}
}
return true; // no further processing of this annotation type
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment