【Java Notes】从直接打印输出HashMap、ArrayList、数组对象到toString()方法(基于JDK8源码)

打印输出三种类型对象的测试程序

之所以想到本次的研究下打印输出HashMapArrayList、数组三种对象实例的结果的原因是我在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

可能我们还比较疑惑的一点是,前面不是返回的类名吗?怎么出来IL这种奇奇怪怪的东西?关于这点,源码getName()的函数说明也给出了解释:

	 * <blockquote><table summary="Element types and encodings">
     * <tr><th> Element Type <th> &nbsp;&nbsp;&nbsp; <th> Encoding
     * <tr><td> boolean      <td> &nbsp;&nbsp;&nbsp; <td align=center> Z
     * <tr><td> byte         <td> &nbsp;&nbsp;&nbsp; <td align=center> B
     * <tr><td> char         <td> &nbsp;&nbsp;&nbsp; <td align=center> C
     * <tr><td> class or interface
     *                       <td> &nbsp;&nbsp;&nbsp; <td align=center> L<i>classname</i>;
     * <tr><td> double       <td> &nbsp;&nbsp;&nbsp; <td align=center> D
     * <tr><td> float        <td> &nbsp;&nbsp;&nbsp; <td align=center> F
     * <tr><td> int          <td> &nbsp;&nbsp;&nbsp; <td align=center> I
     * <tr><td> long         <td> &nbsp;&nbsp;&nbsp; <td align=center> J
     * <tr><td> short        <td> &nbsp;&nbsp;&nbsp; <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()的重写实现,这大概就是多态的魅力。


  1. Java 内部类详解 ↩︎

  2. Java 中 HashMap 初始化时赋值 ↩︎

  3. Java中的所有类都继承自Object,如何实现的呢? ↩︎

  4. java提高篇(十八)-----数组之一:认识JAVA数组 ↩︎

  5. 【java】详解native方法的使用 ↩︎

  6. 原生类 ↩︎

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值