instanceof 实现原理
刷面试题的时候,很多资料都会用题目+答案的情况展示给我们,比如:
//问:下面程序有什么问题吗?运行结果是什么?
"" instanceof Object; //1.true
new String () instanceof String //2.true
‘c’ instanceof Character //3.false
并且在下面给我们解释如下:
注释 1 的返回值是 true,"" 是一个字符串,字符串继承自 Object,所以返回 true。
注释 2 的返回值是 true,因为一个类的对象当然是它的实例了。
注释 3 编译不通过,因为 'c' 是一个 char 类型,也就是一个基本类型,不是一个对象,instanceof 只能用于对象的判断,不能用于基本类型的判断。
这两种题目一下来了十几题,十几个结论,难道要一个个想原因或者背结论?别了,之前一点不了解mysql就想考《阿里巴巴开发规范》,结果发现是不行的,一是MySQL规范看的一头雾水,二是就算想背诵考试蒙混过关也是没意义的,所以就直接找到了朋友,听资深的。给我讲解1个小时。
第二天再重新看MySQL规范,都可以直接理解了,也不用背诵了。
同理。上面的题目一下十几个结论,真的这么多吗?肯定不是。
我们先把试题里面给的结论都拿下来。
注释 1 的返回值是 true,"demo" 是一个字符串,字符串继承自 Object,所以返回 true。 注释 2 的返回值是 true,因为一个类的对象当然是它的实例了。 注释 3 的返回值是 false,因为 Object 是父类,其对象当然不是 String 类的实例了,要注意的是这句话是可以编译通过的,只要 instanceof 关键字的左右两个操作数有继承或实现关系就可以编译通过。 注释 4 编译不通过,因为 'A' 是一个 char 类型,也就是一个基本类型,不是一个对象,instanceof 只能用于对象的判断,不能用于基本类型的判断。 注释 5 返回值是 false,因为这是 instanceof 特有的规则,若左操作数是 null 则结果直接返回 false,不再运算右操作数是什么类,这和我们经常用到的 equals、toString 方法不同。 注释 6 返回值是 false,因为 null 没有类型,所以即使做类型转换还是 null。 注释 7 编译通不过,因为 Date 类和 String 没有继承或实现关系,所以在编译时直接就报错了,instanceof 操作符的左右操作数必须有继承或实现关系,否则编译会失败。 注释 8 返回值是 false,因为虽然 T 是 String 类型且与 Date 之间没有继承或实现关系,但是 Java 泛型在编译成字节码时 T 已经被换成了 Object 类型了(泛型擦出),传递的实参是 String 类型而已。 注释 9 编译不通过,instanceof 的右操作符必须是一个接口或者类,null 啥都不是,所以咯。 注释 10 编译通不过,因为 Date 类和 String 没有继承或实现关系,所以在编译时直接就报错了,instanceof 操作符的左右操作数必须有继承或实现关系,否则编译会失败。 注释 11 返回值为 true,因为数组类型也可以使用 instanceof 来判断。 首先 instanceof 直接对应一条虚拟机指令 instanceof 且为 java 语法保留关键字,而非通过反射实现;instanceof 在底层实现上维护了主要超类型(继承深度)小于一个固定数值(一般为 7)的主数组和次要超类型(判断的时候需要 super 链遍历查找),然后在字节码使用特殊指令对常量池中的相关符号引用进行判断,从而来决定是否某个类或者派生类,以此返回 true 或 false。
这么多结论一下理解也很烦,那就从前浅入深看看源码之类的吧。
首先去官网(https://docs.oracle.com/)看下。具体地址(https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.20.2)。
instanceof操作符的关系表达式操作数的类型必须是引用类型或null类型;否则,将发生编译时错误。 如果instanceof操作符后面提到的引用类型不表示可具体化的引用类型,则是编译时错误(§4.7)。 如果将关系表达式转换为引用类型会被拒绝为编译时错误,那么instanceof关系表达式同样会产生编译时错误。在这种情况下,instanceof表达式的结果永远不可能是真的。 在运行时,如果关系表达式的值不为空,并且可以将引用转换为引用类型(§15.16),而不会引发ClassCastException,则instanceof操作符的结果为true。否则结果为假。
以上为翻译后的官网文档。
果然,结论只有4句,已经减少很多了。看得出来,这只是说了instanceof是怎么设计的,根本和实现原理不搭边。继续往后看。官网的demo也是告诉我们,两个类如果有直接或者间接继承,也就是有父子这两种关系,用instanceof不会编译错误,否则编译错误。没了。
那instanceof为什么会遇到编译错误呢。
用一个demo来说明instanceof,记住这个例子,也就可以大概知道instanceof可以使用的场景了,而且也算是不用instanceof的替代方案了。
boolean result;
if (obj == null) {//首先就把null这种情况排除下,instanceof的左值只要是null,都会返回false
result = false;
}
try {
T temp = (T) obj; //强制类型转换这里,是否所有的都可以转换
result = true;
} catch (ClassCastException e) {
result = false;
}
继续往下看,想起来我之前写过的一篇文章《Java执行前都干了什么?》(https://blog.youkuaiyun.com/qq_20735197/article/details/84837188)在那篇文章中说过编译阶段javac时期,附带javac源码地址。
看下那篇文章2.1.1词法分析
com.sun.tools.javac.parser.Scanner类实现。源代码的字符流转变成标记(token)集合。单个字符是编写程序的最小单位,标记是编译过程的最小单位。关键字、运算符、字面量、变量名都可看成标记。
找到枚举类Token,打开,看下里面有这么一个东西:
public enum Token implements Formattable {
//....
INSTANCEOF("instanceof"),
//....
确实,可以看出instanceof确实是在javac在进行词法分析的时候。
继续往下看,解语法树阶段(http://hg.openjdk.java.net/jdk7u/jdk7u/langtools/file/tip/src/share/classes/com/sun/tools/javac/parser/JavacParser.java)对token的操作:instanceof运算符就会生成这个JCTree.JCInstanceof类型的节点
/** Return operation tag of binary operator represented by token,
* -1 if token is not a binary operator.
*/
static int optag(Token token) {
switch (token) {
//...
case INSTANCEOF:
return JCTree.TYPETEST; //这里吧token的instanceof转成jctree的二进制测试节点
//...
}
}
/* Expression2Rest = {infixop Expression3}
* | Expression3 instanceof Type
* infixop = "||"
* | "&&"
* | "|"
* | "^"
* | "&"
* | "==" | "!="
* | "<" | ">" | "<=" | ">="
* | "<<" | ">>" | ">>>"
* | "+" | "-"
* | "*" | "/" | "%"
*/
JCExpression term2Rest(JCExpression t, int minprec) {
//...
if (topOp == INSTANCEOF) {
return F.at(pos).TypeTest(od1, od2);
} else {
return F.at(pos).Binary(optag(topOp), od1, od2);
}
}
到最后生成字节码的时候为JCTree.JCInstanceof节点生成instanceof字节码指令
public void visitTypeTest(JCInstanceOf tree) {
genExpr(tree.expr, tree.expr.type).load();
code.emitop2(instanceof_, makeRef(tree.pos(), tree.clazz.type));
result = items.makeStackItem(syms.booleanType);
}
不在继续里面一个个方法继续看下去了。但是jvm到这里应该是说一说的,或者一般别人都会问jvm里关于这方面的内容。我也不真知道了,那翻下jvm源码说明呗。(https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.instanceof)
以下为本人翻译,如果不准确请进入上面提供的链接自己看 instanceof 操作说明 判断给定的object的类型 格式 instanceof indexbyte1 indexbyte2 Forms instanceof = 193 (0xc1) 操作栈 ..., objectref → ..., result 说明: objectref必须是从操作栈pop出的参考类型。无符号的indexbyte1和indexbyte2构成了当前类的运行时常量池的index,index的值是indexbyte1<<8|indexbyte2。index的运行时常量池项必须是类、数组或接口类型的符号引用。 如果objectref为空,instanceof指令将一个int结果0作为一个int值推送到操作数堆栈上。 否则,将解析指定的类、数组或接口类型(§5.4.3.1)。如果objectref是已解析类或数组的实例,或者实现了已解析的接口,则instanceof指令将一个int结果1作为int推入操作数堆栈;否则,它将推送一个int结果0。 以下规则用于确定一个非空的objectref是否解决的实例类型:如果S是类对象的引用objectref和T是解决类,数组,或接口类型,instanceof决定objectref T的实例如下: 如果S是一个普通(非数组)类,则: 如果T是类类型,那么S必须与T是同一个类,或者S必须是T的子类; 如果T是接口类型,那么S必须实现接口T。 如果S是接口类型,则: 如果T是一个类类型,那么T必须是Object。 如果T是接口类型,则T必须与S或S的超接口相同。 如果S是表示数组类型SC[]的类,即是SC类型组件的数组,则: 如果T是一个类类型,那么T必须是Object。 如果T是接口类型,那么T必须是数组实现的接口之一(JLS§4.10.3)。 如果T是数组类型TC[],即是数组类型TC的组成部分,则必须满足下列条件之一: TC和SC是相同的原始类型。 TC和SC是引用类型,类型SC可以通过这些运行时规则转换为TC。