不搞流水账,本文详细介绍我们日常开发中使用String类的痛点问题。
你真的了解 toString() 吗?
可能有的小伙伴看到这个问题会不屑一顾,这个玩意老子天天用!
可是,相信你一定有过被 NullPointerException 支配的痛苦经历,而 toString() 方法就是其中一个罪魁祸首!
我先问大家一个问题,是不是所有的Java变量都有toString呢?
如果你觉得有,请把1打在公屏上,如果没有,就打2。(话说我在干嘛,这是博客,又不是Bilibili,emmm,不过可以考虑后期写个弹幕系统)
现在我来公布答案:基本数据类型是没有的,不能使用toString方法!
不相信你可以在Eclipse或者idea里面试一试,编译会报错的。
包装类
上面已经说了,基本数据类型是没有toString的,那么包装类有吗?
包装类,用的比较多的,那就是Integer了,看下Integer有没有toString 呢?答案肯定是有的,包装类不是基本数据类型,那就是有的。
public class StringTest1 { public static void main(String[] args) { Integer i = 1; System.out.println(i.toString()); } }
注意,Integer的toString方法用了重载(jdk1.8版本源码):
public String toString() { return toString(value); }
重新调用了另一个toString,也是写在Integer类中:
public static String toString(int i) { if (i == Integer.MIN_VALUE) return "-2147483648"; int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i); char[] buf = new char[size]; getChars(i, size, buf); return new String(buf, true); }
这些代码如果看的吃力,也没有关系,我们只需要理解大概的意思就行。在调用toString方法的时候,实际上就是把Integer类内部的value属性,转换成一个String对象返回。
Integer自动装箱原理
顺便说一下Integer的自动装箱原理
Integer作为一个典型的包装类,当你写了如下代码:
Integer i = 1;
实际上,编译器会调用Integer的valueOf方法,将原始类型值转换成Integer对象,这个过程我们是感觉不到的。Integer还维护了一个缓存IntegerCache,它会判断i是否在-128和127之间,存在则从IntegerCache中获取包装类的实例,否则new一个新实例。IntegerCache使用了享元模式,其内部维护了一个缓存池,数值范围是-128和127之间,这个缓存是静态的,类加载的时候会自动加载。因为我们正常使用Integer的时候,赋予的值一般不会太大,所以只需牺牲一点点内存,这里用享元模式可以有效地提升效率。
附上Integer的valueOf方法:
public static Integer valueOf(int i) { //判断i是否在-128和127之间,存在则从IntegerCache中获取包装类的实例,否则new一个新实例 if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
这个功能从jdk1.5开始就有了。
Object类
在Java中,所有的类都继承自Object,所以我们有必要再来看看Object的情况。首先是Object类的toString实现:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
可以看到,Object的toString方法就是把对象的hashCode值转为16进制而已。比如:
java.lang.Object@16d3586
但是实际上,调用toString还是会根据当前对象来具体分析的,比方说下面的代码:
Object i = 1; System.out.println(i.toString());
1明明是基本数据类型,但是赋值给Object以后,编译器就会把它看成是Integer对象。不妨做一个实验,在eclipse或者idea中打开调试模式,然后在Integer的toString方法中打上一个断点。当你调试这段代码的时候,就会发现可以进入到Integer的toString方法。
这也就印证了,这种情况编译器是会认识到,当前的1应该是一个Integer对象。
再看一个例子:
Object i = new ArrayList<>(); System.out.println(i.toString());
这时候的i其实是ArrayList的实例对象,所以实际调用toString方法的时候,也就是直接调用ArrayList的toString方法了,而ArrayList并未对toString进行重写,而是ArrayList的父类AbstractList,AbstractList的父类AbstractCollection重写了toString:
————————————————
版权声明:本文为CSDN博主「剽悍一小兔」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_39570751/article/details/122637202
public String toString() { Iterator<E> it = iterator(); if (! it.hasNext()) return "[]"; StringBuilder sb = new StringBuilder(); sb.append('['); for (;;) { E e = it.next(); sb.append(e == this ? "(this Collection)" : e); if (! it.hasNext()) return sb.append(']').toString(); sb.append(',').append(' '); } }
这也是为什么,我们打印ArrayList的时候(这个操作实在是很普遍,懂的都懂),打印出来的是一个数组字符串。
综上,Object类调用toString,如果当前实际的对象没有重写toString,就是调用父类的toString,如果父类也没有重写toString,就层层冒泡最终到Object类。