文章目录
- 前言
- 参考
- 为什么要重写toString方法?
- ”无用的“ 默认的toString()实现
- 如果你的类有良好的toString实现
- 例1: debugger查看实例
- 如何重写toString方法?
- 1. [实现] 返回所有调用方感兴趣的信息
- Q:我们可以用StringBuffer代替StringBuilder吗?
- Q:为什么写成new StringBuilder(40)?
- Q:如果返回的情报不全会如何?
- 2. [文档] 固定格式输出 vs 不固定格式输出
- 固定格式输出
- 不固定格式输出
- 3. [例外] 什么时候不需要重写toString方法
- 工具:AutoValue
- 结语
前言
在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方法的时候我们应该注意什么,应该怎么做?笔者将在本文讨论这些问题。
参考
- Object as a Superclass - Oracle java tutorials
- java.lang.Object - Oracle JSE 13
- 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进制表现
然而,这一实现对我们来说并没有提供任何有用的信息(并不是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方法为你提供的信息。如下图的"str1", “str2”。
如何重写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实例持有的信息如下
- ID
- 姓名
- 性别
- 哈希码(hashCode)
- 类名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
结语
开发者的流动性不能说低,当前一任开发者走掉,继任者接替其工作的时候,项目中的技术债务(坑)越少,则越能快速地让继任者上手。
规范的作业能减少技术债务的参数,对于公司来说能减少人员流动带来的不确定性和开销,对其个人来说也能减轻其日常工作的负担。