操纵JSOM、XML、Java bean等对象时你可能最先想到访问者模式。但是使用访问者模式很难从调用代码控制回调。比如,不能有条件的从所有回调的子分支和叶子节点跳过某个分支。解决这个问题就可以使用Iterator模式遍历整个对象,生成易于开发者阅读和调试的字符串。该迭代器具备一定的通用性,我在使用XPath查找Java对象和在StackHunter中记录异常的工具中都用到了它。
API
本文主要介绍的两个类:StringGenerator和ObjectIterator。
字符串生成器
StringGenerator工具类将对象转化为字符串,使对象可读性更好。可以用它来实现类的toString方法或者把对象的字符串表达作为日志调试代码:
package com.stackhunter.util.tostring.example; import com.stackhunter.example.employee.Department; import com.stackhunter.example.employee.Employee; import com.stackhunter.example.employee.Manager; import com.stackhunter.example.people.Person; import com.stackhunter.util.tostring.StringGenerator; public class StringGeneratorExample { public static void main(String[] args) { Department department = new Department(5775, "Sales") .setEmployees( new Employee(111, "Bill", "Gates"), new Employee(222, "Howard", "Schultz"), new Manager(333, "Jeff", "Bezos", 75000)); System.out.println(StringGenerator.generate(department)); System.out.println(StringGenerator.generate(new int[] { 111, 222, 333 })); System.out.println(StringGenerator.generate(true)); } }
StringGenerator.generate()将department,数组和boolean值进行格式化输出。
com.stackhunter.example.employee.Department@129719f4 deptId = 5775 employeeList = java.util.ArrayList@7037717a employeeList[0] = com.stackhunter.example.employee.Employee@17a323c0 firstName = Bill id = 111 lastName = Gates employeeList[1] = com.stackhunter.example.employee.Employee@57801e5f firstName = Howard id = 222 lastName = Schultz employeeList[2] = com.stackhunter.example.employee.Manager@1c4a1bda budget = 75000.0 firstName = Jeff id = 333 lastName = Bezos name = Sales [I@39df3255 object[0] = 111 object[1] = 222 object[2] = 333 true
对象迭代器
ObjectIterator使用迭代器模式遍历对象的属性,以键值对形式保存。对象中的Java bean、集合、数组及map都要进行迭代。ObjectIterator也会考虑到对象之间循环引用的处理。
package com.stackhunter.util.tostring.example; import com.stackhunter.example.employee.Department; import com.stackhunter.example.employee.Employee; import com.stackhunter.example.employee.Manager; import com.stackhunter.util.objectiterator.ObjectIterator; public class ObjectIteratorExample { public static void main(String[] args) { Department department = new Department(5775, "Sales") .setEmployees( new Employee(111, "Bill", "Gates"), new Employee(222, "Howard", "Schultz"), new Manager(333, "Jeff", "Bezos", 75000)); ObjectIterator iterator = new ObjectIterator("some department", department); while (iterator.next()) { System.out.println(iterator.getName() + "=" + iterator.getValueAsString()); } } }
通过遍历整个对象生成键值对的集合。使用getValueAsString()方法而不是toString()格式化输出。对于原始类型、包装类型、字符串、日期和枚举使用原始的toString()实现。对于其他类型输出类名和hash值。
ObjectIterator.getDepth()会增加缩进,输出更易读。调用next()之前使用nextParent()缩短当前分支跳跃到下一属性。
some department=com.stackhunter.example.employee.Department@780324ff deptId=5775 employeeList=java.util.ArrayList@6bd15108 employeeList[0]=com.stackhunter.example.employee.Employee@22a79c31 firstName=Bill ...
Java对象迭代器的具体实现
实现iterator模式的第一步是创建通用的迭代器接口:IObjectIterator。无论遍历的对象是Java bean、数组还是map都可以使用该接口。
public interface IObjectIterator { boolean next(); String getName(); Object getValue(); }
使用该接口可以按照单一顺序依次获取当前属性的name和value。
实现了IObjectIterator的类用来处理某一种类型的对象。大多数类调用getName()返回名称前缀。ArrayIterator使用了元素的索引:return name + "[" + nextIndex + "]";。
属性迭代器
PropertyIterator可能是最重要的迭代类。它使用Java bean introspection读取对象属性,将它们转化为键值对序列。
public class PropertyIterator implements IObjectIterator { private final Object object; private final PropertyDescriptor[] properties; private int nextIndex = -1; private PropertyDescriptor currentProperty; public PropertyIterator(Object object) { this.object = object; try { BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass()); properties = beanInfo.getPropertyDescriptors(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } @Override public boolean next() { if (nextIndex + 1 >= properties.length) { return false; } nextIndex++; currentProperty = properties[nextIndex]; if (currentProperty.getReadMethod() == null || "class".equals(currentProperty.getName())) { return next(); } return true; } @Override public String getName() { if (currentProperty == null) { return null; } return currentProperty.getName(); } @Override public Object getValue() { try { if (currentProperty == null) { return null; } return currentProperty.getReadMethod().invoke(object); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } } }
数组迭代器
ArrayIterator通过反射得到数组的长度,进而检索每个数据元素。ArrayIterator不关心从getValue()方法返回值的具体细节。它们一般情况下被传递给PropertyIterator。
public class ArrayIterator implements IObjectIterator { private final String name; private final Object array; private final int length; private int nextIndex = -1; private Object currentElement; public ArrayIterator(String name, Object array) { this.name = name; this.array = array; this.length = Array.getLength(array); } @Override public boolean next() { if (nextIndex + 1 >= length) { return false; } nextIndex++; currentElement = Array.get(array, nextIndex); return true; } @Override public String getName() { return name + "[" + nextIndex + "]"; } @Override public Object getValue() { return currentElement; } }
集合迭代器
CollectionIterator与ArrayIterator非常相似。使用java.lang.Iterable调用它的Iterable.iterator()方法初始化内部迭代器。
Map迭代器
MapIterator遍历java.util.Map的entry。它并不深入到每个entry的键值对,这个工作由MapEntryIterator类完成。
public class MapIterator implements IObjectIterator { private final String name; private Iterator<?> entryIterator; private Map.Entry<?, ?> currentEntry; private int nextIndex = -1; public MapIterator(String name, Map<?, ?> map) { this.name = name; this.entryIterator = map.entrySet().iterator(); } @Override public boolean next() { if (entryIterator.hasNext()) { nextIndex++; currentEntry = (Entry<?, ?>) entryIterator.next(); return true; } return false; } ... }
Map Entry迭代器
MapEntryIterator处理java.util.Map的单个entry。它只返回两个值:entry的键和值。与ArrayIterator及其他的类似,如果是复杂类型的话,它的结果可能最终传递给PropertyIterator,作为Java bean处理。
根迭代器
RootIterator返回单个元素——初始节点。可以把它想成XML文件的根节点。目的是发起整个遍历过程。
整合
ObjectIterator类作为门面角色(Facade),包装了所有的遍历逻辑。它根据最后一次getValue()的返回值类型决定哪个IObjectIterator的子类需要实例化。当子迭代器在内部创建时它在栈中保存当前迭代器的状态。它也暴露了getChild()和getDepth()方法为调用者展示当前进度。
private IObjectIterator iteratorFor(Object object) { try { if (object == null) { return null; } if (object.getClass().isArray()) { return new ArrayIterator(name, object); } if (object instanceof Iterable) { return new CollectionIterator(name, (Iterable<?>) object); } if (object instanceof Map) { return new MapIterator(name, (Map<?, ?>) object); } if (object instanceof Map.Entry) { return new MapEntryIterator(name, (Map.Entry<?, ?>) object); } if (isSingleValued(object)) { return null; } return new PropertyIterator(object); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e.getMessage(), e); } }
字符串生成器的实现
已经看到如何遍历对象中的所有属性。最后的工作就是让输出更加美观,以及增加一些限制条件(比如不能创建一个GB级大小的字符串)。
public static String generate(Object object) { String s = ""; ObjectIterator iterator = new ObjectIterator("object", object); ... while (iterator.next()) { if (s.length() >= MAX_STRING_LENGTH) { return s; } if (iterator.getChild() >= MAX_CHILDREN) { iterator.nextParent(); continue; } String valueAsString = iterator.getValueAsString(); s += System.lineSeparator(); s += indent(iterator.getDepth()) + truncateString(iterator.getName()); if (valueAsString == null) { s += " = null"; } else { s += " = " + truncateString(valueAsString); } } return s; }
代码第21行完成了格式化,增加了缩进和层次结构使显示更美观。
同时增加了一些限制条件:
第9行——限制字符串长度在16k内。
第13行——限制任何父节点下子节点数量小于64.
第21&25行——限制键和值长度在64个字符。
总结
本文介绍了如何使用迭代器模式遍历包含各种复杂属性的对象。关键是将每种类型的迭代委派给各自的类实现。可以在你自己的软件中使用这两个工具。