[Java] 为什么要重写toString方法?如何写?

前言


在Oracle的某一Java的文档里,有这样一段话,”你应该总是重写在你的类里重写toString()方法。“

You should always consider overriding the toString() method in your classes.
The Object’s toString() method returns a String representation of the object, which is very useful for debugging. The String representation for an object depends entirely on the object, which is why you need to override toString() in your classes.

推荐了我们应该重写该方法。但是为什么要重写toString方法呢?在重写toString方法的时候我们应该注意什么,应该怎么做?笔者将在本文讨论这些问题。

参考


  1. Object as a Superclass - Oracle java tutorials
  2. java.lang.Object - Oracle JSE 13
  3. java.lang.Object类源码 - OpenJDK7

为什么要重写toString方法?


如果用一句话来说,那就是被良好编写的toString方法能大大增加你debug的效率。
无论你是在开发环境用debugger、还是用生产环境的log去debug,良好的toString方法都会极大地方便你去debug。但是默认的toString实现是无法满足我们debug的需求的,也是为什么我们要重写toString方法的一个原因。

”无用的“ 默认的toString()实现


相信大家打印List的时候都有过这样的经历,如下↓

[YourClassName@6e0be858, YourClassName@61bbe9ba, ...]

其实上面这一串很奇怪而且没什么鸟用的东西,就是默认的toString()实现了。

// OpenJDK7 java.lang.Object类toString()源码
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

这一实现其实在Object类的javadoc里已经有明确的说明

你的类名(带package,如java.lang.Object) + @符号 + hashCode的16进制表现
toString方法的javadoc

然而,这一实现对我们来说并没有提供任何有用的信息(并不是informative representation),我们debug时看到如@61bbe9ba这种信息的时候只会想砍人。

如果你的类有良好的toString实现

toString方法在很多地方都会被调用

  • 当你print一个对象的时候,会自动调用toString方法。
  • 当你print一个collection的时候,其内部所持有的每个对象都会被分别调用toString方法。
  • 当你在开发环境调用debugger看类实例的时候,大部分debugger都会调用实例的toString方法为你显示其内部的信息。(例1
  • 当你把一个对象写到log文件里的时候,会自动调用toString方法。
  • 当你用断言(Assert)检查两个实例是否相同时,会自动调用toString方法。
  • etc…

如果你有良好的toString实现,那么在这些地方你将会看到非常有用的信息帮助你debug(掌握程序当时的内部状态)。

例1: debugger查看实例

如果你没有重写toString方法,debug的时候将无法直观看到实例内部的信息。
不重写toString
但如果你重写了toString方法,那么你将能非常直观地看到toString方法为你提供的信息。如下图的"str1", “str2”。
重写toString方法

如何重写toString方法?


在上一节,我们了解了为什么要重写toString方法(why),在本节我们将去了解如何重写toString方法(how)。

1. [实现] 返回所有调用方感兴趣的信息


一个通常的实现是你应该在toString方法里返回调用方(Client)感兴趣的信息(Interesting Information)。例如你有一个Person类如下。

public static class Person {
    private String identity; // ID (E.g. 身份证号码)
    private String name; // 姓名
    private boolean gender; // 性别,false: 男性 true: 女性

    private int hashCode;
    @Override public int hashCode() { ... } // 实现略
}

一个Person实例持有的信息如下

  1. ID
  2. 姓名
  3. 性别
  4. 哈希码(hashCode)
  5. 类名Person

那么大概率我们感兴趣的信息是前三者(ID、姓名、性别)。而不是后两者(hashCode和类名)。
值得一提的是这里谁需要被输出并没有标准答案,后两者是否需要添加到toString的输出里,取决于类的开发者,如果开发者认为类名和hashCode需要被输出,那么请把其添加到toString的输出里。

笔者在下面提供了Person类toString方法的简单实现,供参考。

※值得注意的是笔者之前使用的Eclipse Mar提供了自动生成toString方法的实现的时候,但生成的代码是通过”field1” + field1这种字符串连接的方法实现的,这会非常影响性能。笔者推荐使用StringBuilder来完成字符串拼接的操作。

@Override public String toString() {
    StringBuilder sb = new StringBuilder(40); // 40是预计容量(capacity)
    final String genderStr = gender? "女性": "男性";
    sb.append('{')
            .append("identity=").append(identity).append(',')
            .append("name=").append(name).append(',')
            .append("gender=").append(genderStr)
            .append('}');

    return sb.toString();
}

Q:我们可以用StringBuffer代替StringBuilder吗?


答案是可以但不推荐,StringBuffer和StringBuilder的区别,大家应该都很熟悉了。笔者简单提一下就是前者(StringBuffer)是线程安全的,而StringBuilder是非线程安全的。线程安全意味着需要同步耗费性能。在toString内部,并没有多线程调用的场景,我们只需用StringBuilder类做字符串拼接即可。

Q:为什么写成new StringBuilder(40)?


答案是为了改善性能,大多数提供了初始容量(initial capacity)构造器的容器类都有一个逻辑是当内部空间不够用的时候需要去扩展容量。每次扩展容量都会产生额外的开销,如果你能大致算出容量的情况下,最好还是把初始容量写上。这里40是笔者通过估计(数)得到的估计值,开发者需要根据自己类的情况来填入不同的值。

虽然这会加重前期开发的负担,但是相比稀缺的计算资源来讲,这一点负担完全值得开发者去承担。

当然这不是必须的,当你完全没有可能遭遇性能瓶颈的时候,如你这个类的toString方法只可能被调用有限次的情况,你可以放心地不指定初始容量而使用默认值。

StringBuilder就是一个典型,当其容量不够时会通过expandCapacity方法用倍增+2的方式来增加其容量。

//OpenJDK源码地址 - https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/lang/AbstractStringBuilder.java
void expandCapacity(int minimumCapacity) {
    int newCapacity = value.length * 2 + 2;
    if (newCapacity - minimumCapacity < 0)
        newCapacity = minimumCapacity;
    if (newCapacity < 0) {
        if (minimumCapacity < 0) // overflow
            throw new OutOfMemoryError();
        newCapacity = Integer.MAX_VALUE;
    }
    value = Arrays.copyOf(value, newCapacity);
}

而另一典型的ArrayList则被认为是每次扩展为原容量的1.5倍(OpenJDK源码)。

Q:如果返回的情报不全会如何?


设想你只返回Person实例的姓名(name)属性,那么当你有两个Person实例都叫"Jack"的时候,你将无法知道究竟自己寻找的”Jack“是这之中的哪一位,情报不全会导致你难以精准定位问题。所以尽可能在toString方法内提供外部感兴趣的完整的情报!虽然这很主观(无标准)且难以把握。

2. [文档] 固定格式输出 vs 不固定格式输出


初次看到“固定格式输出”和“不固定格式输出”或许难以理解,软件模块之间的交互接口是一种约定(Contract),接口的描述(文档)就是其对外部(Client)做出的承诺(约定)。toString方法也不例外,是一个接口,需要提供文档向外描述它究竟做了什么。

固定格式输出


固定格式输出即为,在文档中明确说明输出的格式,并标明输出格式固定。在之后的release不做变更(向即下兼容),外部可放心对其做输出的字符串做逆向解析


如本例,在文档中申明了格式固定,那么外部工具则可以放心的解析输出字符串里的前三个字段。

/**
 * 返回该实例的字符串表现。
 *
 * 返回的字符串的格式一定如下:
 * {identity=identity值,name=name值,gender="男性"|"女性"[,新增字段=新增字段的值]}
 *
 * 分隔符为','。
 */
@Override public String toString() { ... }

不固定格式输出


在文档中简单说明当前版本的格式,但标明格式并非固定,后续release可能会对其格式做变更(不保证向下兼容),外部即使对其当前版本的输出字符串做了逆向解析,也可能因为后续变更导致解析工具失效。


在本例,对外说明此时不保证向下兼容,对外提示说请勿对文本内容做解析,后续变更可能会导致解析工具失效。

/**
 * 返回该实例的字符串表现。
 *
 * 返回的字符串的格式不固定,后续版本可能会变更,但大致输出的信息如下
 * {identity=identity值,name=name值,gender="男性"|"女性"[,新增字段=新增字段的值]}
 */
@Override public String toString() { ... }

3. [例外] 什么时候不需要重写toString方法


toString方法主要是为了对外提供实例的信息。而有的情况,我们完全不会用到其toString方法的时候,我们则不太需要重写其toString方法。

静态工具类 (Static Utility Class)
这些类通常没有实例,因此没有必要为其重写toString方法。

枚举类 (Enum Types)
大多数情况下,枚举类的默认toString实现返回的name,已经足够了。

 /**
  * Returns the name of this enum constant, as contained in the
  * declaration.  This method may be overridden, though it typically
  * isn't necessary or desirable.  An enum type should override this
  * method when a more "programmer-friendly" string form exists.
  *
  * @return the name of this enum constant
  */
 public String toString() {
     return name;
 }

除此之外的大多数情况,你都应该重写toString方法。特别是能实例化的类(instantiable class)。

工具:AutoValue

AutoValue是Google的开源项目,是一个代码生成器(Code Generator),能够帮你快速生成代码,如HashCode、toString等。因为篇幅和时间原因,笔者在本文将不会介绍AutoValue的用法,有兴趣的读者可以在网上查询该工具的使用方法。
AutoValue源码 - github

结语


开发者的流动性不能说低,当前一任开发者走掉,继任者接替其工作的时候,项目中的技术债务(坑)越少,则越能快速地让继任者上手。
规范的作业能减少技术债务的参数,对于公司来说能减少人员流动带来的不确定性和开销,对其个人来说也能减轻其日常工作的负担。
我是虎猫,希望本文对你有所帮助。

### 回答1: Java中的toString方法是Object类中的一个方法,它返回一个字符串,用于表示该对象的字符串表示形式。默认情况下,toString方法返回的字符串是对象的类名和哈希码的十六进制表示。 然而,这种默认的toString方法往往不能满足我们的需求,因为它只提供了对象的基本信息,而没有提供更多的有用信息。因此,我们需要重写toString方法,以便返回更有用的信息,例如对象的属性值、状态等。 重写toString方法可以使我们更方便地调试和输出对象的信息,也可以使我们的代码更易于阅读和理解。因此,重写toString方法Java编程中的一个常见实践。 ### 回答2: Java中的toString()是一个非常有用的方法,它是Object类中的方法,它返回该对象的字符串表示形式。默认情况下,toString()方法将返回对象的类名,后跟“@”符号和十六进制哈希码。然而,在某些情况下,这种默认的字符串表示形式可能无法满足实际需求,因此我们需要重写toString()方法。 首先,重写toString()方法可以提高代码的可读性和可维护性。默认情况下,toString()方法返回的字符串可能只是一个十六进制哈希码,这对于调试和理解代码是没有帮助的。如果我们为对象的属性定义了一个良好的字符串表示形式,则可以通过在toString()方法中返回该字符串来大大简化代码。 其次,重写toString()方法可以为对象的输出提供更多的控制。例如,如果我们希望在输出对象时仅显示其部分属性,则可以在toString()方法中仅包含这些属性。这将帮助我们更好地控制输出和优化性能,特别是在大型应用程序中。 此外,toString()方法还是调试对象时的有用工具。通过覆盖toString()方法,我们可以为对象定义一个易于阅读和理解的字符串表示形式,从而使调试更加简单和高效。 总之,在Java重写toString()方法是一种将对象转换为字符串的惯用方法,它为我们提供了一种更有效的方式来理解和控制对象的输出。虽然默认的toString()方法在某些情况下可能足够满足我们的需求,但通过覆盖它,我们可以进一步提高代码的可读性、可维护性和可控制性。 ### 回答3: Java是一种面向对象的编程语言,其中的每个对象都有自己的特性和属性,因此也需要有相应的方法来表示和操作这些对象。在Java中,toString方法就是其中一个常用的方法,它是Object类的一个方法,用于将对象转换成字符串,便于输出和观察对象的属性。 然而,Object类中的toString方法只是简单地返回对象的类名和散列码,如果开发人员需要输出更具体的信息,就需要重写这个方法。以下是Java重新实现toString()的一些原因: 1.方便调试 使用Java的调试器时,toString方法可以向开发人员输出有关对象的详细信息,例如对象的属性和状态。这样,开发人员就可以更有效地调试代码和找出问题所在。 2.提供更好的日志信息 在Java应用程序中,通常需要生成日志信息来记录应用程序的运行情况。通过重写toString方法,可以更好地记录对象的状态和变化,以及实现更高质量的日志记录。 3.更好的代码可读性 Java中的toString方法还可以用于调试、测试和优化代码,因为它可以帮助开发人员更好地理解代码中各个部分之间的关系。当在代码中使用toString时,代码会变得更加容易阅读和理解。 4.更好的代码隔离性 JavatoString方法还可以用于与其他Java程序交互。当重写toString方法时,开发人员可以控制对象输出的信息,从而控制代码的可读性和隔离性。 总的来说,JavatoString方法可以帮助开发人员更好地理解和调试代码,优化日志记录和开发过程,提高代码的可读性和可维护性,从而提高Java程序的质量和性能。因此,JAVA需要重写toString方法
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值