Java: как избежать циклических ссылок при сбросе информации об объектах с отражением?

Я изменил метод дампа объекта, чтобы избежать циклических ссылок, вызывающих ошибку Stackru. Вот чем я закончил:

//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);

Итак, в основном я добавил excludeList, который содержит все ранее проверенные объекты. Теперь, если объект содержит другой объект и этот объект связывается с исходным объектом, он не должен следовать за этим объектом дальше по цепочке.

Тем не менее, моя логика, кажется, имеет недостаток, так как я все еще застреваю в бесконечном цикле. Кто-нибудь знает, почему это происходит?

РЕДАКТИРОВАТЬ:

Я все еще получаю ошибку Stackru

Exception in thread "AWT-EventQueue-0" java.lang.StackruError
    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 ответ

Решение

+1 за использование IdentityHashMap решить проблему.

Причина в том, что ваш метод в настоящее время зависит от того, как реализует класс каждого посещаемого объекта equals, поскольку List.contains(Object) использует equals в качестве основы для сравнения. Если класс equals() метод сломан и неправильно возвращает false даже если передать себя в качестве объекта сравнения, вы получите бесконечный цикл, потому что вызов List.contains всегда возвращает false, и этот подграф объекта всегда просматривается для этого типа объекта.

Кроме того, если у вас есть два или более объектов, которые являются различными экземплярами, но считаются равными по значению (т. Е. Возвращает значение true), только один из них будет записан. Желательно ли это или проблема зависит от вашего варианта использования.

Используя IdentityHashMap позволит избежать обеих этих проблем.

В сторону - если вы хотите сделать отступ в соответствии с глубиной вызова, не забудьте увеличить callCount на рекурсивных вызовах dumpFields ,

РЕДАКТИРОВАТЬ: Я думаю, что код работает правильно. Проблема в том, что вы действительно получаете переполнение стека. Это произойдет, если у вас большой граф объектов. Например, представьте себе связанный список из 3000 элементов. Для этого потребуется 3000 рекурсивных вызовов, которые, я уверен, увеличат размер стека по умолчанию.

Чтобы это исправить, вы либо увеличиваете размер стека (vmarg -Xss) до достаточно большого, чтобы обрабатывать ожидаемый размер объекта (не надежное решение!), Либо заменяете использование стека явной структурой данных.

Вместо стека создайте рабочий список. Этот список содержит объекты, которые вы видели, но еще не обработали. Вместо того, чтобы рекурсивно вызывать dumpFields, вы просто добавляете объект в свой рабочий список. Основным элементом метода является цикл while, который выполняет итерацию, пока в списке есть элементы.

Например

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();

РЕДАКТИРОВАТЬ 2: Я просто запустил код, чтобы увидеть, что происходит. Есть 3 основных изменения, необходимых для этой работы:

  1. Тест для примитивных типов должен быть первым тестом (первым из 3-х операторов if.), Вторым, если if является тестом карты исключений.
  2. Тест для примитивных типов должен включать проверки для ВСЕХ примитивных классов. В настоящее время у вас есть несколько тестов, но поплавок, двойной, байтовый, короткий и длинный отсутствуют.
  3. Пропустить статические поля, проверить Modifier.isStatic(field.getModifiers()),

Причина, по которой примитивные тесты должны выполняться в первую очередь, заключается в том, что при отражении тип примитива упаковывается с использованием нового экземпляра соответствующего класса (например, для двойного поля новый Double создан - это упрощение - JDK будет фактически повторно использовать некоторые объекты, см. источники для Integer.valueOf() но в целом новый объект создается, когда примитив упакован.) Поскольку эти примитивы генерируют уникальные новые объекты, нет смысла проверять их по карте исключения. Поэтому сначала поставьте тест на примит. Кстати, проверка value.getClass().isPrimitive() всегда будет возвращать false - упакованный тип никогда не будет примитивным типом. Вместо этого вы можете использовать объявленный тип поля, например field.getType().isPrimitive(),

Тест против классов в штучной упаковке примитивов должен включать тесты для всех классов в штучной упаковке примитивов. Если этого не произойдет, то эти новые объекты в штучной упаковке будут продолжать создаваться, будучи обнаруженными не исключенными (так как они являются новыми экземплярами) и добавленными в рабочий список. Это становится безудержной проблемой - статические открытые конечные константы, такие как MAX_VALUE, вызывают генерацию большего количества экземпляров, которые добавляются в список, а отражение полей этих объектов вызывает больше значений и т. Д. Исправление заключается в том, чтобы гарантировать, что все примитивные типы проверено на (или используйте isPrimitive для типа поля, а не типа возвращаемого объекта.)

Отсутствие вывода статических полей послужит вспомогательным решением проблемы, описанной выше, но, что более важно, это избавит ваш вывод от загромождения ненужными деталями.

Другие вопросы по тегам