Java: как избежать циклических ссылок при сбросе информации об объектах с отражением? - PullRequest
1 голос
/ 30 апреля 2010

Я изменил метод дампа объекта, чтобы избежать циклических ссылок, вызывающих ошибку 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);

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

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

EDIT:

Я все еще получаю ошибку 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 Ответ

4 голосов
/ 30 апреля 2010

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

EDIT2: Я просто запустил код, чтобы увидеть, что происходит. Есть 3 основных изменения, необходимых для этой работы:

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

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

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

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

...