【Java Notes】从直接打印输出HashMap、ArrayList、数组对象到toString()方法(基于JDK8源码)
打印输出三种类型对象的测试程序
之所以想到本次的研究下打印输出HashMap
、ArrayList
、数组三种对象实例的结果的原因是我在leetcode刷题过程中发现直接输出ArrayList<Integer>
对象实例可以打印动态数组内的数据,结合日常输出对象实例的情况,我觉得有必要看看这个输出的底层是怎样的,所以我编写了以下测试程序:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* 测试System.out.println直接输出HashMap、ArrayList、数组
*/
public class JavaTest1 {
public static void main(String[] args) {
//HashMap
HashMap<String, String> map = new HashMap<String, String>(){ //匿名内部类:HashMap的子类
{ //实例初始化代码块
put("a","b");
put("c","d");
}
};
System.out.println(map);
System.out.println(map.toString());
//ArrayList
ArrayList<String> list = new ArrayList<String>(){
{
add("e");
add("f");
}
};
System.out.println(list);
System.out.println(list.toString());
//数组
int[] array = new int[]{1,2,3,4,5};
String[] strArray = new String[]{"1","2","3"};
System.out.println(array);
System.out.println(array.toString());
System.out.println(strArray);
System.out.println(strArray.toString());
}
}
输出结果如下:
{a=b, c=d}
{a=b, c=d}
[e, f]
[e, f]
[I@1b6d3586
[I@1b6d3586
[Ljava.lang.String;@4554617c
[Ljava.lang.String;@4554617c
刚刚程序示例中有几处需要注意的地方。
一是定义一个HashMap
对象时,采用了匿名内部类1的方式(也是在leetcode刷题过程中见到过),匿名内部类是局部内部类的特例,本质是一个继承了类或者实现了接口的子类匿名对象,所以这个匿名内部类实际是HashMap
的子类对象2 ;
理解了外层{}
内实际就是一个类的定义之后,那么再来理解内层{}
包括的代码块,内层{}
的代码块叫做构造代码块,构造代码块在类中的成员方法外出现,一般它的作用是将多个构造方法中相同的代码存放到一起,每次调用构造方法(创建对象)都执行,并且在构造方法前执行,一般对对象进行初始化。
HashMap<String, String> mapOverride = new HashMap<String, String>(){ //匿名内部类:HashMap的子类
{ //实例初始化,构造代码块
put("a","b");
put("c","d");
}
}
二是示例程序中的对象定义过程中,存在着大量的多态(具体类多态和接口多态)。在实际调用成员方法时,调用的是变量实际指向的对象的成员方法。
源码解析(基于JDK8)
从输出函数到toString()方法
从输出结果开始追踪,查看输出函数,发现直接以对象为输出函数println
实参时,底层重载的函数原型是:
public void println(Object x) {
String s = String.valueOf(x);//转化字符串
synchronized (this) {
print(s);
newLine(); //换行
}
}
我们看到,输入参数是一个Object
类型的变量,因为Java当中的所有类(除了Object
类)都是继承Object
类的3,所以这里是一个类的多态;
使用 synchronized
关键字加锁,保证同一时刻只有一个输出流;
输出内容的关键代码是String s = String.valueOf(x);
继续查看该静态函数的源码:
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
可以看到最终调用的是obj
对象的toString()
方法。
调用谁的toString()方法?
既然刚刚提到了println
函数的输入参数那里存在类的多态,那么输出结果不同的根据大概是找到了,就是在于不同对象调用了不同的toString()
方法。
先明确一点,toString()
方法是Object
基类中定义的方法,当我们从类的继承树上都不到toString()
的重写时,最后必然调用的是Object
基类中的方法。
现在我们来分析一下当输出HashMap
对象、ArrayList
对象和数组对象时的底层调用。
一、 HashMap
对象
因为在HashMap
源码中没有找到toString()
方法,所以需要沿着继承树向上寻找它的父类中有没有该方法。这里先看下HashMap
的类图:
我们向上寻找它的父类AbstractMap
(而不是在Map
接口中寻找,因为接口不继承任何类),在其中找到了toString()
方法的重写:
public String toString() {
Iterator<Entry<K,V>> i = entrySet().iterator(); //定义迭代器
if (! i.hasNext()) //空map返回{}
return "{}";
StringBuilder sb = new StringBuilder();
sb.append('{');
for (;;) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
sb.append(key == this ? "(this Map)" : key); //当K是定义的泛型时实际对象类型自身
//https://blog.csdn.net/yangyingbofx/article/details/51259063
sb.append('=');
sb.append(value == this ? "(this Map)" : value); //当V是定义的泛型时实际对象类型自身
if (! i.hasNext())
return sb.append('}').toString();
sb.append(',').append(' ');
}
}
从源码中可以看到,这里的toString()
方法就是遍历这个map并输出键值对。
PS:这里面Entry<K,V>
是一个定义在Map
接口内部的一个接口,通过追踪HashMap
内定义的entrySet()
函数一步步深入,可以发现它的实现类是HashMap
的静态内部类static class Node<K,V> implements Map.Entry<K,V>
。
二、 ArrayList
对象
先看看类图:
同理,ArrayList
源码中没有找到toString()
方法,我们向上寻找它的父类AbstractList
(而不是在List
接口中寻找,因为接口不继承任何类),没有找到;继续向上寻找父类AbstractCollection
,在其中找到了toString()
:
public String toString() {
Iterator<E> it = iterator(); //定义迭代器
if (! it.hasNext()) //空list返回
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e); //当E是定义的泛型时实际对象类型自身 //https://blog.csdn.net/yangyingbofx/article/details/51259063
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
可以看到内容被显示的关键是sb.append(e == this ? "(this Collection)" : e);
这句,追踪进去发现:
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
好的,套娃开始,剩下的就又回到了开头。
三、数组对象
对于数组对象,我们似乎对它是一个对象的感觉没有特别的感觉。实际上,数组对象是一个特殊的对象,并且数组是Object
基类的直接子类4。所以我们如果追踪数组对象的toString()
方法,最终应该会在Object
基类的源码中找到它:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
看到这,再看看测试程序中的输出,[I@1b6d3586
和[Ljava.lang.String;@4554617c
,看到@
就八九不离十了。Object
基类的这个toString()
方法返回的就是一个拼接起来的字符串,@
之前是类名,之后是该对象的哈希值转十六进制的数。值得注意的是getClass()
和hashCode()
方法都是native
方法5。
可能我们还比较疑惑的一点是,前面不是返回的类名吗?怎么出来I
和L
这种奇奇怪怪的东西?关于这点,源码getName()
的函数说明也给出了解释:
* <blockquote><table summary="Element types and encodings">
* <tr><th> Element Type <th> <th> Encoding
* <tr><td> boolean <td> <td align=center> Z
* <tr><td> byte <td> <td align=center> B
* <tr><td> char <td> <td align=center> C
* <tr><td> class or interface
* <td> <td align=center> L<i>classname</i>;
* <tr><td> double <td> <td align=center> D
* <tr><td> float <td> <td align=center> F
* <tr><td> int <td> <td align=center> I
* <tr><td> long <td> <td align=center> J
* <tr><td> short <td> <td align=center> S
* </table></blockquote>
这些HTML标签实际是一个表格(我估计是为生成网页文档所以才这样写,具体没有探究),直接写一个HTML显示下(把下面附加的例子也显示了下):
我们可以看到对于类和接口,输出的是Lclassname;
,对于原生类6(即基本数据类型)而言,输出的就是显示单字母关键字。
那为什么数组对象的输出会有个[
?源码中也有解释:If this class object represents a class of arrays, then the internal form of the name consists of the name of the element type preceded by one or more '{@code [}' characters representing the depth of the array nesting.
,意思就是用[
表示数组嵌套的深度(上面例子中的最后一个就是),后面再跟上类型名。
延伸思考1:重写toString()方法
既然涉及到多态,那我们可以试试重写一下toString()
方法:
public class JavaTest2 {
public static void main(String[] args) {
//对象重写toString()方法
HashMap<String, String> mapOverride = new HashMap<String, String>(){ //匿名内部类:HashMap的子类,泛型必须填写上
{ //实例初始化,构造代码块
put("a","b");
put("c","d");
}
@Override //注解,加上@Override可以让编译器帮助检查是否进行了正确的覆写。希望进行覆写,但是不小心写错了方法签名,编译器会报错。
public String toString() {
return "Map Override";
}
};
System.out.println(mapOverride);
System.out.println(mapOverride.toString());
//HashMap内存储对象重写toString()方法
HashMap<Student, String> newMap = new HashMap<Student, String>(){ //匿名内部类:HashMap的子类
//https://blog.csdn.net/zdyueguanyun/article/details/79216998
//https://www.cnblogs.com/yanggb/p/11590225.html
{ //实例初始化代码块
put(new Student(),"小A");
put(new Student(),"小B");
}
};
System.out.println(newMap);
System.out.println(newMap.toString());
//ArrayList内存储对象重写toString()方法
ArrayList<Student> newlist = new ArrayList<Student>(){
{
add(new Student());
add(new Student());
}
};
System.out.println(newlist);
System.out.println(newlist.toString());
//数组内存储对象重写toString()方法
Student[] b = new Student[]{
new Student(), new Student()
};
System.out.println(b);
System.out.println(b.toString());
Map<String,String>[] a = new HashMap[]{mapOverride}; //以实际new的对象类型为准
System.out.println(a);
System.out.println(a.toString());
}
}
class Student{
@Override
public String toString() {
return "Student";
}
}
输出结果:
Map Override
Map Override
{Student=小A, Student=小B}
{Student=小A, Student=小B}
[Student, Student]
[Student, Student]
[LJavabeginner.Student;@677327b6
[LJavabeginner.Student;@677327b6
[Ljava.util.HashMap;@14ae5a5
[Ljava.util.HashMap;@14ae5a5
延伸思考2:嵌套式的ArrayList
直接上测试程序:
public class JavaTest3 {
public static void main(String[] args) {
//嵌套式ArrayList
List<List<Integer>> newListNesting = new ArrayList<>();
List<Integer> temp = new ArrayList<>();
temp.add(1);
newListNesting.add(new ArrayList<>(temp));
temp.add(2);
newListNesting.add(new ArrayList<>(temp));
System.out.println(newListNesting);
System.out.println(newListNesting.toString());
}
}
输出结果:
[[1], [1, 2]]
[[1], [1, 2]]
这个输出结果一点都不奇怪,根据前面讲的内容来看,嵌套进入也是无限套娃,一层接一层的调用toString()
,只不过要注意调用者的实际类型去找不同的toString()
的重写实现,这大概就是多态的魅力。