使用Iterator模式将对象转成String

高洛峰
高洛峰 原创
2016-12-13 17:46:48 1090浏览

操纵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 + "]";。

1.png

属性迭代器

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个字符。

总结

本文介绍了如何使用迭代器模式遍历包含各种复杂属性的对象。关键是将每种类型的迭代委派给各自的类实现。可以在你自己的软件中使用这两个工具。


声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn核实处理。