Java:如何在使用反射转储对象信息时避免循环引用?

时间:2010-04-30 16:58:34

标签: java reflection stack-overflow dump circular-reference

我修改了一个对象转储方法,以避免导致StackOverflow错误的循环引用。这就是我最终的结果:

//returns all fields of the given object in a string
 public static String dumpFields(Object o, int callCount, ArrayList excludeList)
 {
  //add this object to the exclude list to avoid circual references in the future
  if (excludeList == null) excludeList = new ArrayList();
  excludeList.add(o);

  callCount++;
  StringBuffer tabs = new StringBuffer();
  for (int k = 0; k < callCount; k++)
  {
   tabs.append("\t");
  }
  StringBuffer buffer = new StringBuffer();
  Class oClass = o.getClass();
  if (oClass.isArray()) {   
   buffer.append("\n");
   buffer.append(tabs.toString());
   buffer.append("[");
   for (int i = 0; i < Array.getLength(o); i++)
   {
    if (i < 0) buffer.append(",");
    Object value = Array.get(o, i);

    if (value != null)
    {
     if (excludeList.contains(value))
     {
      buffer.append("circular reference");
     }
     else if (value.getClass().isPrimitive() || value.getClass() == java.lang.Long.class || value.getClass() == java.lang.String.class || value.getClass() == java.lang.Integer.class || value.getClass() == java.lang.Boolean.class)
     {
      buffer.append(value);
     }
     else
     {
      buffer.append(dumpFields(value, callCount, excludeList));
     }
    }
   }
   buffer.append(tabs.toString());
   buffer.append("]\n");
  }
  else
  {   
   buffer.append("\n");
   buffer.append(tabs.toString());
   buffer.append("{\n");
   while (oClass != null)
   {    
    Field[] fields = oClass.getDeclaredFields();
    for (int i = 0; i < fields.length; i++)
    {
     if (fields[i] == null) continue;

     buffer.append(tabs.toString());
     fields[i].setAccessible(true);
     buffer.append(fields[i].getName());
     buffer.append("=");
     try
     {
      Object value = fields[i].get(o);
      if (value != null)
      {
       if (excludeList.contains(value))
       {
        buffer.append("circular reference");
       }
       else if ((value.getClass().isPrimitive()) || (value.getClass() == java.lang.Long.class) || (value.getClass() == java.lang.String.class) || (value.getClass() == java.lang.Integer.class) || (value.getClass() == java.lang.Boolean.class))
       {
        buffer.append(value);
       }
       else
       {
        buffer.append(dumpFields(value, callCount, excludeList));
       }
      }
     }
     catch (IllegalAccessException e)
     {
      System.out.println("IllegalAccessException: " + e.getMessage());
     }
     buffer.append("\n");
    }
    oClass = oClass.getSuperclass();
   }
   buffer.append(tabs.toString());
   buffer.append("}\n");
  }
  return buffer.toString();
 }

该方法最初调用如下:

System.out.println(dumpFields(obj, 0, null);

所以,基本上我添加了一个包含所有previousely检查对象的excludeList。现在,如果一个对象包含另一个对象,并且该对象链接回原始对象,则它不应该在链的下方跟随该对象。

然而,我的逻辑似乎有一个缺陷,因为我仍然陷入无限循环。有谁知道为什么会这样?

修改

我仍然收到StackOverflow错误

Exception in thread "AWT-EventQueue-0" java.lang.StackOverflowError
    at java.lang.reflect.Field.copy(Field.java:127)
    at java.lang.reflect.ReflectAccess.copyField(ReflectAccess.java:122)
    at sun.reflect.ReflectionFactory.copyField(ReflectionFactory.java:289)
    at java.lang.Class.copyFields(Class.java:2739)
    at java.lang.Class.getDeclaredFields(Class.java:1743)
    at com.gui.ClassName.dumpFields(ClassName.java:627)

我的更新方法:

public static String dumpFields(Object o, int callCount, IdentityHashMap idHashMap)
    {
        callCount++;

        //add this object to the exclude list to avoid circual references in the future
        if (idHashMap == null) idHashMap = new IdentityHashMap();
        idHashMap.put(o, o);

        //setup string buffer and add fields
        StringBuffer tabs = new StringBuffer();
        for (int k = 0; k < callCount; k++)
        {
            tabs.append("\t");
        }
        StringBuffer buffer = new StringBuffer();
        Class oClass = o.getClass();
        if (oClass.isArray()) {         
            buffer.append("\n");
            buffer.append(tabs.toString());
            buffer.append("[");
            for (int i = 0; i < Array.getLength(o); i++)
            {
                if (i < 0) buffer.append(",");
                Object value = Array.get(o, i);

                if (value != null)
                {
                    if (idHashMap.containsKey(value))
                    {
                        buffer.append("circular reference");
                    }
                    else if (value.getClass().isPrimitive() || value.getClass() == java.lang.Long.class || value.getClass() == java.lang.String.class || value.getClass() == java.lang.Integer.class || value.getClass() == java.lang.Boolean.class)
                    {
                        buffer.append(value);
                    }
                    else
                    {
                        buffer.append(dumpFields(value, callCount, idHashMap));
                    }
                }
            }
            buffer.append(tabs.toString());
            buffer.append("]\n");
        }
        else
        {           
            buffer.append("\n");
            buffer.append(tabs.toString());
            buffer.append("{\n");
            while (oClass != null)
            {               
                Field[] fields = oClass.getDeclaredFields();
                for (int i = 0; i < fields.length; i++)
                {
                    if (fields[i] == null) continue;

                    buffer.append(tabs.toString());
                    fields[i].setAccessible(true);
                    buffer.append(fields[i].getName());
                    buffer.append("=");
                    try
                    {
                        Object value = fields[i].get(o);
                        if (value != null)
                        {
                            if (idHashMap.containsKey(value))
                            {
                                buffer.append("circular reference");
                            }
                            else if ((value.getClass().isPrimitive()) || (value.getClass() == java.lang.Long.class) || (value.getClass() == java.lang.String.class) || (value.getClass() == java.lang.Integer.class) || (value.getClass() == java.lang.Boolean.class))
                            {
                                buffer.append(value);
                            }
                            else
                            {
                                buffer.append(dumpFields(value, callCount, idHashMap));
                            }
                        }
                    }
                    catch (IllegalAccessException e)
                    {
                        System.out.println("IllegalAccessException: " + e.getMessage());
                    }
                    buffer.append("\n");
                }
                oClass = oClass.getSuperclass();
            }
            buffer.append(tabs.toString());
            buffer.append("}\n");
        }
        return buffer.toString();
    }

EDIT2:

你的解决方案似乎非常好。不幸的是我现在得到一个OutOfMemory错误,即使我只在一个只有4个字段的小类上使用它。这是我最终得到的代码:

//returns all fields of the given object in a string
    public static String dumpFields(Object start)
    {
        class CallLevel
        {
            public Object target;
            public int level;

            public CallLevel(Object target, int level)
            {
                this.target = target;
                this.level = level;
            }
        }

        //create a work list
        List<CallLevel> workList = new ArrayList<CallLevel>();
        workList.add(new CallLevel(start, 0));

        //add this object to the exclude list to avoid circual references in the future
        IdentityHashMap idHashMap = new IdentityHashMap();

        StringBuffer buffer = new StringBuffer();
        while (!workList.isEmpty())
        {
            CallLevel level = workList.remove(workList.size() - 1);
            Object o = level.target;

            //add this object to the exclude list to avoid circual references in the future
            idHashMap.put(o, o);

            //setup string buffer and add fields
            StringBuffer tabs = new StringBuffer();
            int callCount = level.level;
            for (int k = 0; k < callCount; k++)
            {
                tabs.append("\t");
            }
            callCount++;
            Class oClass = o.getClass();

            if (oClass.isArray()) {         
                buffer.append("\n");
                buffer.append(tabs.toString());
                buffer.append("[");
                for (int i = 0; i < Array.getLength(o); i++)
                {
                    if (i < 0) buffer.append(",");
                    Object value = Array.get(o, i);

                    if (value != null)
                    {
                        if (idHashMap.containsKey(value))
                        {
                            buffer.append("circular reference");
                        }
                        else if (value.getClass().isPrimitive() || value.getClass() == java.lang.Long.class || value.getClass() == java.lang.String.class || value.getClass() == java.lang.Integer.class || value.getClass() == java.lang.Boolean.class)
                        {
                            buffer.append(value);
                        }
                        else
                        {
                            workList.add(new CallLevel(value, callCount));
                        }
                    }
                }
                buffer.append(tabs.toString());
                buffer.append("]\n");
            }
            else
            {           
                buffer.append("\n");
                buffer.append(tabs.toString());
                buffer.append("{\n");
                while (oClass != null)
                {               
                    Field[] fields = oClass.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++)
                    {
                        if (fields[i] == null) continue;

                        buffer.append(tabs.toString());
                        fields[i].setAccessible(true);
                        buffer.append(fields[i].getName());
                        buffer.append("=");
                        try
                        {
                            Object value = fields[i].get(o);
                            if (value != null)
                            {
                                if (idHashMap.containsKey(value))
                                {
                                    buffer.append("circular reference");
                                }
                                else if ((value.getClass().isPrimitive()) || (value.getClass() == java.lang.Long.class) || (value.getClass() == java.lang.String.class) || (value.getClass() == java.lang.Integer.class) || (value.getClass() == java.lang.Boolean.class))
                                {
                                    buffer.append(value);
                                }
                                else
                                {
                                    workList.add(new CallLevel(value, callCount));
                                }
                            }
                        }
                        catch (IllegalAccessException e)
                        {
                            System.out.println("IllegalAccessException: " + e.getMessage());
                        }
                        buffer.append("\n");
                    }
                    oClass = oClass.getSuperclass();
                }
                buffer.append(tabs.toString());
                buffer.append("}\n");
            }
        }
        return buffer.toString();
    }

对于这么小的对象,它不应该导致OutOfMemory错误。

有什么想法吗?

EDIT3:

重写版本:

public static String dumpFields(Object start)
    {
        class CallLevel
        {
            public Object target;
            public int level;

            public CallLevel(Object target, int level)
            {
                this.target = target;
                this.level = level;
            }
        }

        //create a work list
        List<CallLevel> workList = new ArrayList<CallLevel>();
        workList.add(new CallLevel(start, 0));

        //create an identity map for object comparison
        IdentityHashMap idHashMap = new IdentityHashMap();

        //setup a string buffer to return
        StringBuffer buffer = new StringBuffer();
        while (!workList.isEmpty())
        {
            CallLevel level = workList.remove(workList.size() - 1);
            Object o = level.target;

            //add this object to the exclude list to avoid circual references in the future
            idHashMap.put(o, o);

            //set string buffer for tabs
            StringBuffer tabs = new StringBuffer();
            int callCount = level.level;
            for (int k = 0; k < callCount; k++)
            {
                tabs.append("\t");
            }

            //increment the call count for future calls
            callCount++;

            //set the class for this object
            Class oClass = o.getClass();

            //if this is an array, dump it's elements, otherwise dump the fields of this object
            if (oClass.isArray()) {         
                buffer.append("\n");
                buffer.append(tabs.toString());
                buffer.append("[");
                for (int i = 0; i < Array.getLength(o); i++)
                {
                    if (i < 0) buffer.append(",");
                    Object value = Array.get(o, i);

                    if (value != null)
                    {
                        if (value.getClass().isPrimitive())
                        {
                            buffer.append(value);
                        }
                        else if (idHashMap.containsKey(value))
                        {
                            buffer.append("circular reference");
                        }
                        else
                        {
                            workList.add(new CallLevel(value, callCount));
                        }
                    }
                }
                buffer.append(tabs.toString());
                buffer.append("]\n");
            }
            else
            {           
                buffer.append("\n");
                buffer.append(tabs.toString());
                buffer.append("{\n");
                while (oClass != null)
                {               
                    Field[] fields = oClass.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++)
                    {
                        //make sure this field exists
                        if (fields[i] == null) continue;

                        //ignore static fields
                        if (!Modifier.isStatic(fields[i].getModifiers()))
                        {
                            buffer.append(tabs.toString());
                            fields[i].setAccessible(true);
                            buffer.append(fields[i].getName());
                            buffer.append("=");
                            try
                            {
                                Object value = fields[i].get(o);
                                if (value != null)
                                {
                                    if (fields[i].getType().isPrimitive())
                                    {
                                        buffer.append(value);
                                    }
                                    else if (idHashMap.containsKey(value))
                                    {
                                        buffer.append("circular reference");
                                    }
                                    else
                                    {
                                        workList.add(new CallLevel(value, callCount));
                                    }
                                }
                            }
                            catch (IllegalAccessException e)
                            {
                                System.out.println("IllegalAccessException: " + e.getMessage());
                            }
                            buffer.append("\n");
                        }
                    }
                    oClass = oClass.getSuperclass();
                }
                buffer.append(tabs.toString());
                buffer.append("}\n");
            }   
        }
        return buffer.toString();
    }

我假设getClass()。isPrimitive()仍然适用于数组索引,但我可能错了。如果是这样,你会怎么处理?另外,其他getClass()== Integer等检查对我来说似乎没必要,因为isPrimitive()检查应该处理这个,对吗?

无论如何,当在一个简单的对象上使用时,我仍然会出现内存不足错误:

Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOfRange(Arrays.java:3209)
    at java.lang.String.<init>(String.java:215)
    at java.lang.StringBuffer.toString(StringBuffer.java:585)
    at com.gui.ClassName.dumpFields(ClassName.java:702)
    at com.gui.ClassName.setTextArea(ClassName.java:274)
    at com.gui.ClassName.access$8(ClassName.java:272)
    at com.gui.ClassName$1.valueChanged(ClassName.java:154)
    at javax.swing.JList.fireSelectionValueChanged(JList.java:1765)
    at javax.swing.JList$ListSelectionHandler.valueChanged(JList.java:1779)
    at javax.swing.DefaultListSelectionModel.fireValueChanged(DefaultListSelectionModel.java:167)
    at javax.swing.DefaultListSelectionModel.fireValueChanged(DefaultListSelectionModel.java:147)
    at javax.swing.DefaultListSelectionModel.fireValueChanged(DefaultListSelectionModel.java:194)
    at javax.swing.DefaultListSelectionModel.changeSelection(DefaultListSelectionModel.java:388)
    at javax.swing.DefaultListSelectionModel.changeSelection(DefaultListSelectionModel.java:398)
    at javax.swing.DefaultListSelectionModel.setSelectionInterval(DefaultListSelectionModel.java:442)
    at javax.swing.JList.setSelectedIndex(JList.java:2179)
    at com.gui.ClassName$1.valueChanged(ClassName.java:138)
    at javax.swing.JList.fireSelectionValueChanged(JList.java:1765)
    at javax.swing.JList$ListSelectionHandler.valueChanged(JList.java:1779)
    at javax.swing.DefaultListSelectionModel.fireValueChanged(DefaultListSelectionModel.java:167)
    at javax.swing.DefaultListSelectionModel.fireValueChanged(DefaultListSelectionModel.java:137)
    at javax.swing.DefaultListSelectionModel.setValueIsAdjusting(DefaultListSelectionModel.java:668)
    at javax.swing.JList.setValueIsAdjusting(JList.java:2110)
    at javax.swing.plaf.basic.BasicListUI$Handler.mouseReleased(BasicListUI.java:2783)
    at java.awt.AWTEventMulticaster.mouseReleased(AWTEventMulticaster.java:273)
    at java.awt.Component.processMouseEvent(Component.java:6263)
    at javax.swing.JComponent.processMouseEvent(JComponent.java:3255)
    at java.awt.Component.processEvent(Component.java:6028)
    at java.awt.Container.processEvent(Container.java:2041)
    at java.awt.Component.dispatchEventImpl(Component.java:4630)
    at java.awt.Container.dispatchEventImpl(Container.java:2099)
    at java.awt.Component.dispatchEvent(Component.java:4460)

1 个答案:

答案 0 :(得分:4)

+1使用IdentityHashMap来解决问题。

原因是您的方法当前取决于每个访问对象的类如何实现equals,因为List.contains(Object)使用equals作为比较的基础。如果类的equals()方法被破坏,并且即使将自身作为比较对象传递时也错误地返回false,那么您将获得无限循环,因为对List.contains的调用始终返回false并且对象子图总是遍历该类型的对象。

此外,如果您有两个或多个不同实例的对象,但被视为等于值(即等于返回true),则只会写出其中一个。这是否可取或问题取决于您的使用案例。

使用IdentityHashMap可以避免这两个问题。

抛开 - 如果你想根据调用深度缩进,不要忘记递归callCount递归调用{/ 1}}。

编辑:我认为代码工作正常。问题是你真的得到了堆栈溢出。如果您有一个大对象图,就会发生这种情况。例如,想象一下3000个元素的链表。这将涉及3000个递归调用,我非常肯定会使用默认堆栈大小来破坏堆栈。

要解决此问题,您可以将堆栈的大小(vmarg -Xss)增加到足以处理预期的对象图形大小(不是一个强大的解决方案!),或者使用显式数据结构替换堆栈的使用

创建工作清单而不是堆栈。此列表包含您已看到但尚未处理的对象。您只需将对象添加到工作列表中,而不是递归调用dumpFields。该方法的主体是一个while循环,只要列表中有项目就会迭代。

E.g。

dumpFields

EDIT2: 我刚刚运行代码看看会发生什么。完成这项工作需要进行3项主要更改:

  1. 原始类型的测试应该是第一个测试(3个if语句中的第一个)。第二个测试是否是对排除映射的测试。
  2. 原始类型的测试需要包括对所有原始类的检查。您目前有一些测试,但缺少float,double,byte,short和long。
  3. 跳过静态字段,检查class CallLevel { CallLevel(Object target, int level) { this.target = target; this.level = level; } Object target; int level; } public static String dumpFields(Object start) { List<CallLevel> workList = new ArrayList<CallLevel>(); workList.add(new Calllevel(start,0)); Map idHashMap = new IdentityHashMap(); while (!workList.isEmpty()) { CallLevel level = workList.removeAt(workList.size()-1); Object o = level.object; //add this object to the exclude list to avoid circual references in the future idHashMap.put(, o); //setup string buffer and add fields StringBuffer tabs = new StringBuffer(); int callCount = level.level; for (int k = 0; k < callCount; k++) { tabs.append("\t"); } callCount++; StringBuffer buffer = new StringBuffer(); Class oClass = o.getClass(); if (oClass.isArray()) { buffer.append("\n"); buffer.append(tabs.toString()); buffer.append("["); for (int i = 0; i < Array.getLength(o); i++) { if (i < 0) buffer.append(","); Object value = Array.get(o, i); if (value != null) { if (idHashMap.containsKey(value)) { buffer.append("circular reference"); } else if (value.getClass().isPrimitive() || value.getClass() == java.lang.Long.class || value.getClass() == java.lang.String.class || value.getClass() == java.lang.Integer.class || value.getClass() == java.lang.Boolean.class) { buffer.append(value); } else { workList.add(new Calllevel(value, callCount)); } } } buffer.append(tabs.toString()); buffer.append("]\n"); } else { buffer.append("\n"); buffer.append(tabs.toString()); buffer.append("{\n"); while (oClass != null) { Field[] fields = oClass.getDeclaredFields(); for (int i = 0; i < fields.length; i++) { if (fields[i] == null) continue; buffer.append(tabs.toString()); fields[i].setAccessible(true); buffer.append(fields[i].getName()); buffer.append("="); try { Object value = fields[i].get(o); if (value != null) { if (idHashMap.containsKey(value)) { buffer.append("circular reference"); } else if ((value.getClass().isPrimitive()) || (value.getClass() == java.lang.Long.class) || (value.getClass() == java.lang.String.class) || (value.getClass() == java.lang.Integer.class) || (value.getClass() == java.lang.Boolean.class)) { buffer.append(value); } else { workList.add(new CallLevel(value, callCount)); } } } catch (IllegalAccessException e) { System.out.println("IllegalAccessException: " + e.getMessage()); } buffer.append("\n"); } oClass = oClass.getSuperclass(); } buffer.append(tabs.toString()); buffer.append("}\n"); } return buffer.toString();
  4. 原始测试首先应该发生的原因是,使用反射时,原始类型使用相应类的新实例加框(例如,对于双字段,创建新的Modifier.isStatic(field.getModifiers()) - 这是一个简单的 - JDK实际上会重用一些对象,查看Double的源代码,但一般情况下,当一个基元被装箱时会创建一个新对象。)由于这些原始元素生成了唯一的新对象,因此对于排除映射几乎没有必要检查它们。因此,首先进行原始测试。顺便提一下,检查Integer.valueOf()将始终返回false - 盒装类型永远不是基本类型。您可以使用声明的字段类型,例如value.getClass().isPrimitive()

    针对盒装基元类的测试必须包括所有盒装原始类的测试。如果没有,那么将继续创建这些新的盒装对象,发现它们不被排除(因为它们是新实例)并添加到工作列表中。这成为一个失控的问题 - 像MAX_VALUE这样的静态公共最终常量导致生成更多实例,这些实例被添加到列表中,并且这些对象的字段的反映会导致更多的值等...修复是为了确保所有基本类型都是测试(或在字段类型上使用isPrimitive,而不是返回的对象类型。)

    不输出静态字段将作为上述问题的辅助修复,但更重要的是它可以避免输出混乱的不必要的细节。