Java Specialists' Java Training Europehome of the java specialists' newsletter

The Java Specialists' Newsletter
Issue 0792003-10-08 Category: Language Java version:

Subscribe Free RSS Feed

toString() Genérico

by Dr. Heinz M. Kabutz
Special Thank You! I would like to thank Rafael Steil from the Grupo de Usuarios Java - GUJ, from Brazil for translating our newsletters into Portuguese. In addition, I would like to thank Vanessa Sabino for assisting Rafael in the translation work. Disclaimer: Since I do not know any Portuguese, I make no warranty for the accuracy of the translation. Heinz

Would you like to really understand Java concurrency? Join us for an in-depth study of how threading works in Java. During the course, you will learn how to write correct and fast multi-threaded Java code. Please click here if you would like to learn more.

toString() Genérico

Gostaria de agradecer Ravi Nukala pela idéia nesta newsletter.

Esta newsletter explora uma forma de você usar um método toString() genérico que converte qualquer objeto Java para um String, ou adiciona-o a um StringBuffer. Uma restrição do sistema atual é que um tratamento circular irá resultar em recursão infinita, o que causa erros de vazamento de pilha no Java. Talvez isto possa ser contornado mantendo um histórico de quais objetos já foram visitados. Mas isto complicaria o sistema substancialmente e portanto decidi não tratar disso aqui.

Quando pergunto para uma platéia grande quais Design Patterns eles conhecem, normalmente recebo pelo menos essas três respostas: Singleton, Factory e Facade. Singleton parece ser orientado a objetos, porém da forma que a maioria das pessoas usa ele não é. Factory não aparece "as is" no livro do Gang-of-Four (GoF, or Erich Gamma et al). Há um Abstract Factory e um Factory Method, ambos sendo bem diferentes do método estático para criação de objetos normalmente chamado de "Factory". E então há o Facade, que eu pessoalmente não considero realmente um Design Pattern no sentido tradicional. Se você ler o livro, irá notar que a seção sobre a estrutura do Facade é diferente dos outros patterns. Existe uma razão para isso: o Facade está lá para ajudá-lo a classificar o número enorme de classes que você obtém quando usa um monte de patterns. O Facade é muitas vezes confundido com uma interface para um subsistema, mas ele é mais do que apenas uma interface: ele também deveria permitir acesso direto ao subsistema, o que uma interface normalmente não permite. No código exemplo desta newsletter, veremos um Facade que “torna o subsistema mais fácil de usar”.

Nosso Facade constrói três Converters (conversores) e organiza-os na Chain-Of-Responsibility. Então ele passa cada objeto para a cadeia, que lida com ele chamando o método "handle()".

/**
 * O comportamento padrão é iniciar com o NullConverter,
 * seguido do ArrayConverter e então o ObjectConverter.
 */
public class ToStringFacade {
  private final static ToStringConverter INITIAL =
      new NullConverter(
          new ArrayConverter(
              new ObjectConverter(null)));
  public static String toString(Object o) {
    return toString(o, new StringBuffer()).toString();
  }
  public static StringBuffer toString(Object o, StringBuffer buf) {
    return INITIAL.handle(o, buf);
  }
}
  

Para começar, definimos uma classe Utils que retorna o nome da classe sem o pacote. Esta classe é utilizada em vários conversores.

public class Utils {
  /** Queremos apenas ver o nome da classe, sem o pacote. */
  public static String stripPackageName(Class c) {
    return c.getName().replaceAll(".*\\.", "").replace('$', '.');
  }
}
  

Em seguida, definimos uma classe abstrata que iremos utilizar como a principal classe de tratamento na Chain-of-Responsibility. Isto evita ter um if-else multi-condicional que precisaria decidir qual Strategy usar para um objeto particular.

/**
 * Aqui usamos o design pattern Chain-of-Responsibility, que é
 * muito similar ao Strategy pattern, exceto que passamos adiante
 * o pedido se não podemos lidar com ele. Isso ajuda a reduzir
 * o número de if-else if multi condicionais que precisaríamos
 * no nosso programa para decidir qual Strategy utilizar.
 */
public abstract class ToStringConverter {
  private final ToStringConverter successor;
  /** Precisamos saber o sucessor, caso este tratamento não possa
   * lidar com o pedido. */
  protected ToStringConverter(ToStringConverter successor) {
    this.successor = successor;
  }
  /** handle() decide se o objeto atual pode tratar o pedido;
   * caso contrário ele passa adiante para o próximo na seqüência. */
  protected final StringBuffer handle(Object o, StringBuffer buf) {
    if (!isHandler(o)) {
      assert successor != null;
      return successor.handle(o, buf);
    }
    return toString(o, buf);
  }
  /** Subclasses especificam se podem tratar o objeto atual. */
  protected abstract boolean isHandler(Object o);
  /** O método toString() é o método principal chamado de dentro
   * do método handle(), uma vez que tenha sido estabelecido qual objeto
   * deve tratar o pedido. */
  protected StringBuffer toString(Object o, StringBuffer buf) {
    buf.append('(');
    appendName(o, buf);
    buf.append(getSeparator());
    appendValues(o, buf);
    buf.append(')');
    return buf;
  }
  /** O separador entre nome e valor pode ser diferente para
   * diferentes tipos de objetos. */
  protected char getSeparator() { return '='; }
  /** Este método determinará um identificador para o objeto atual. */
  protected void appendName(Object o, StringBuffer buf) { }
  /** Este método determinará os valores para o objeto atual */
  protected void appendValues(Object o, StringBuffer buf) { }
}
  

O caso mais fácil de lidar é quando o objeto é null. Neste caso sempre permitimos que o toString() adicione "(null)":

/** Esta classe segue o Null Object Pattern do Bobby Woolf. */
public class NullConverter extends ToStringConverter {
  public NullConverter(ToStringConverter successor) {
    super(successor);
  }
  /** Este tratamento é usado apenas se o objeto for null */
  protected boolean isHandler(Object o) {
    return o == null;
  }
  protected StringBuffer toString(Object o, StringBuffer buf) {
    buf.append("(null)");
    return buf;
  }
}
  

O próximo caso que deve ser considerado é quando o objeto está dentro de uma array. Nós gostaríamos de lidar com arrays multi-dimensionais. Além disso, queremos ter certeza que podemos lidar com classes que não tem seu próprio método toString(). Portanto usamos nossa própria classe ToStringConverter a não ser que os componentes da array sejam primitivos, outras arrays ou Strings.

import java.lang.reflect.Array;

/** Este conversor suporta somente Arrays. Ele suporta
 * arrays de primitivos e arrays multi dimensionais. */
public class ArrayConverter extends ToStringConverter {
  public ArrayConverter(ToStringConverter successor) {
    super(successor);
  }
  /** Este tratamento funciona apenas para arrays. */
  protected boolean isHandler(Object o) {
    return o.getClass().isArray();
  }
  /** Queremos adicionar o tipo da array e o número de
   * dimensões, ex. para uma array tridimensional queremos
   * adicionar [][][].  Usando += para o sufixo String não é o
   * ideal, mas a maioria dos casos será provavelmente de arrays
   * unidimensionais, em que não aconteceria nenhuma
   * concatenação de Strings. */
  protected void appendName(Object o, StringBuffer buf) {
    assert o.getClass().isArray();
    String postfix = "[]";
    Class c = o.getClass().getComponentType();
    while(c.isArray()) {
      postfix += "[]";
      c = c.getComponentType();
    }
    buf.append(Utils.stripPackageName(c));
    buf.append(postfix);
  }
  /** Mostramos a array usando as dimensões e o métodos 
   * toString() dos valores. Este método é recursivo para
   * tratar arrays multi-dimensionais. */
  protected void appendValues(Object o, StringBuffer buf) {
    assert o.getClass().isArray();
    buf.append('{');
    int length = Array.getLength(o);
    for (int i = 0; i < length; i++) {
      Object value = Array.get(o, i);
      if (value != null && value.getClass().isArray()) {
        appendValues(value, buf);
      } else if (o.getClass().getComponentType().isPrimitive()) {
        buf.append(value);
      } else if (value instanceof String) {
        buf.append('"').append(value).append('"');
      } else {
        ToStringFacade.toString(value, buf);
      }
      if (i < length - 1) {
        buf.append(',');
      }
    }
    buf.append('}');
  }
}
  

O tratamento mais complicado é o de objetos gerais. Queremos mostrar o nome da classe e todos os campos com seus valores. Isto é feito recursivamente.

import java.lang.reflect.*;
import java.util.*;

public class ObjectConverter extends ToStringConverter {
  public ObjectConverter(ToStringConverter successor) {
    super(successor);
  }
  /** Este é o ponto final da cadeia. Se chegar aqui, 
   * usamos este tratamento. */
  protected boolean isHandler(Object o) {
    return true;
  }
  /** Especificamos um separador diferente entre nome
   * e valores para o output ficar mais bonito. */
  protected char getSeparator() { return ':'; }
  /** Para o nome da classe, retiramos o nome do pacote. */
  protected void appendName(Object o, StringBuffer buf) {
    buf.append(Utils.stripPackageName(o.getClass()));
  }
    /** Os valores são um pouco mais complicados. Primeiro temos que
   * encontrar todos os campos. Para encontrar os campos privados, 
   * temos que percorrer recursivamente a hierarquia de objetos. Então
   * passamos por todos os campos e adicionamos nome e valor, exceto
   * quando chegamos ao fim, que adicionamos ", ". */
  protected void appendValues(Object o, StringBuffer buf) {
    Iterator it = findAllFields(o);
    while(it.hasNext()) {
      Field f = (Field) it.next();
      appendFieldName(f, o, buf);
      buf.append('=');
      appendFieldValue(f, o, buf);
      if (it.hasNext()) {
        buf.append(", ");
      }
    }
  }
  /** Se a classe do campo não é a classe do objeto (ex. o campo
   * está declarado em uma superclasse) então incluímos o
   * nome da superclasse junto com o nome do campo. */
  private void appendFieldName(Field f, Object o, StringBuffer buf) {
    if (f.getDeclaringClass() != o.getClass()) {
      buf.append(Utils.stripPackageName(f.getDeclaringClass()));
      buf.append('.');
    }
    buf.append(f.getName());
  }
  /** Definimos o campo para ser “acessível”, ou seja, público. Se o
   * tipo do campo é primitivo, apenas adicionamos o valor;
   * caso contrário, recursivamente incluímos o valor usando nosso
   * ToStringFacade. */
  private void appendFieldValue(Field f, Object o, StringBuffer buf) {
    try {
      f.setAccessible(true);
      Object value = f.get(o);
      if (f.getType().isPrimitive()) {
        buf.append(value);
      } else {
        ToStringFacade.toString(value, buf);
      }
    } catch (IllegalAccessException e) {
      assert false : "We have already set it accessible!";
    }
  }
  /** Encontra todos os campos de um objeto, seja privado ou público.
   * Também olhamos os campos nas superclasses. */
  private Iterator findAllFields(Object o) {
    Collection result = new LinkedList();
    Class c = o.getClass();
    while (c != null) {
      Field[] f = c.getDeclaredFields();
      for (int i = 0; i < f.length; i++) {
        if (!Modifier.isStatic(f[i].getModifiers())) {
          result.add(f[i]);
        }
      }
      c = c.getSuperclass();
    }
    return result.iterator();
  }
}
  

A última classe que precisamos é o tão necessário teste unitário. Ele permitirá que você faça refactoring das classes como desejar, sem preocupar-se muito sobre quebrar qualquer coisa.

import java.util.*;
import junit.framework.TestCase;
import junit.swingui.TestRunner;

public class ToStringTest extends TestCase {
  public static void main(String[] args) {
    TestRunner.run(ToStringTest.class);
  }
  public ToStringTest(String name) {
    super(name);
  }
  public void testPackageStripping() {
    assertEquals("Integer",
        Utils.stripPackageName(Integer.class));
    assertEquals("Map.Entry",
        Utils.stripPackageName(Map.Entry.class));
    assertEquals("ToStringTest",
        Utils.stripPackageName(ToStringTest.class));
  }
  public void testNull() {
    assertEquals("(null)", ToStringFacade.toString(null));
  }
  public void testInteger() {
    assertEquals("(Integer:value=42)",
        ToStringFacade.toString(new Integer(42)));
  }
  public void testString() {
    assertEquals("(String:value=(char[]={H,e,l,l,o, ,W,o,r,l,d,!}), " +
        "offset=0, count=12, hash=0)",
        ToStringFacade.toString("Hello World!"));
  }
  public void testArray() {
    assertEquals("(int[]={1,2,3})",
        ToStringFacade.toString(new int[]{1, 2, 3}));
  }
  public void testMultiArray() {
    assertEquals("(long[][][][]={{{{1,2,3},{4}},{{5}}}})",
        ToStringFacade.toString(
            new long[][][][]{{{{1, 2, 3}, {4}}, {{5}}}}));
  }
  public void testTestClass() {
    assertEquals("(ToStringTest.TestClass:names=" +
        "(String[]={\"Heinz\",\"Joern\",\"Pieter\",\"Herman\"" +
        ",\"John\"}), totalIQ=900)",
        ToStringFacade.toString(new TestClass()));
  }
  private static class TestClass {
    private final String[] names = {"Heinz", "Joern", "Pieter",
                              "Herman", "John"};
    private final int totalIQ = 180 * 5;
  }
  public void testArrayList() {
    ArrayList list = new ArrayList();
    list.add("Heinz");
    list.add("Helene");
    list.add("Maxi");
    list.add("Connie");
    assertEquals("(ArrayList:elementData=(Object[]={\"Heinz\"" +
        ",\"Helene\",\"Maxi\",\"Connie\",(null),(null),(null)," +
        "(null),(null),(null)}), size=4, AbstractList.modCount=4)",
        ToStringFacade.toString(list));
  }
}
  

Isso conclui a newsletter desta semana.Meu aviso esta semana é que eu não usei esta abordagem em nenhum sistema em produção. Provavelmente é um tanto ineficiente fazer isto desta forma e pode haver formas para você otimizar. Algumas vezes é conveniente ter um método que exponha o conteúdo de um objeto, incluindo campos provados da superclasse.

Language Articles Related Java Course Discuss at The Java Specialist Club

    
Your Name

Your E-Mail

Your Phone

Your Company

Your Comment


Java Master
Java Concurrency
Design Patterns
In-House Courses



© 2010 Heinz Kabutz - All Rights Reserved Sitemap seo web design Catch22 Marketing
Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners. JavaSpecialists.eu is not connected to Oracle, Inc. and is not sponsored by Oracle, Inc.