JSOM, XML, Java Bean 및 기타 객체를 조작할 때 먼저 방문자 패턴을 생각할 수 있습니다. 그러나 방문자 패턴을 사용하면 호출 코드에서 콜백을 제어하기가 어렵습니다. 예를 들어 모든 콜백 하위 분기 및 리프 노드에서 분기를 조건부로 건너뛸 수 없습니다. 이 문제를 해결하려면 반복자 패턴을 사용하여 전체 개체를 탐색하고 개발자가 쉽게 읽고 디버그할 수 있는 문자열을 생성할 수 있습니다. 이 반복자는 매우 다재다능하며 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()는 출력을 위해 부서, 배열 및 부울 값의 형식을 지정합니다. .
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, 컬렉션, 배열 및 맵을 반복해야 합니다. 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()); } } }
전체 객체를 순회하여 키-값 쌍 컬렉션을 생성합니다. toString() 대신 getValueAsString() 메서드를 사용하여 출력 형식을 지정합니다. 기본 유형, 래핑된 유형, 문자열, 날짜 및 열거형에는 원래 toString() 구현을 사용하십시오. 다른 유형의 경우 클래스 이름과 해시 값이 출력됩니다.
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 객체 반복자의 구체적인 구현
반복자 패턴을 구현하는 첫 번째 단계는 일반 반복자 인터페이스인 IObjectIterator를 만드는 것입니다. 이 인터페이스는 탐색된 객체가 Java Bean, 배열 또는 맵인지 여부에 관계없이 사용할 수 있습니다.
public interface IObjectIterator { boolean next(); String getName(); Object getValue(); }
이 인터페이스를 사용하면 현재 속성의 이름과 값을 단일 순서로 얻을 수 있습니다.
IObjectIterator를 구현하는 클래스는 특정 유형의 개체를 처리하는 데 사용됩니다. 대부분의 클래스는 getName()을 호출하여 이름 접두사를 반환합니다. ArrayIterator는 요소의 인덱스를 사용합니다: 반환 이름 + "[" + nextIndex + "]";.
Property Iterator
PropertyIterator는 아마도 가장 중요한 반복 클래스일 것입니다. 이는 Java Bean 내부 검사를 사용하여 객체 속성을 읽고 이를 일련의 키-값 쌍으로 변환합니다.
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); } } }
Array Iterator
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个字符。
总结
本文介绍了如何使用迭代器模式遍历包含各种复杂属性的对象。关键是将每种类型的迭代委派给各自的类实现。可以在你自己的软件中使用这两个工具。