面试常见考题_java基础(全)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
J2SE基础
➢九种基本数据类型的大小,以及他们的封装类。
基本类型:boolean byte char short int long float double void
●基本数据类型与其对应的封装类由于本质的不同,具有一些区别:
基本数据类型只能按值传递,而封装类按引用传递。
基本类型在堆栈中创建;而对于对象类型,对象在堆中创建,对象的引用在堆栈中创建。
基本类型由于在堆栈中,效率会比较高,但是可能会存在内存泄漏的问题。
➢2. Switch能否用string做参数?
在java7之前,switch只支持byte、short、char、int 或者其对应的封装类以及Eumn类型,
➢3. equals与==的区别。
值类型是存储在内存的栈中,引用类型的变量在栈中存放其引用类型变量的地址,其值存储在堆中。
== 比较的是两个变量的值是否相等,对于引用变量表示的是两个变量在堆中存储的地址是否相同,即栈中的内容是否相同。
函数的参数,新建了一个对象并存储在内存中的另一个位置。
(如果常量不存在与字符串缓冲区,则先在字符串缓冲区中建立常量,然后newString,这样就创建了两个对象!)
java字符串缓冲池分析java字符串缓冲池分析: java的虚拟机在内存中开辟出一块单
独的区域,用来存储字符串对象,这块内存区域被称为字符串缓冲池。
到底创建了几个对象?1,0,2,1,1
➢4. Object有哪些公用方法?
●Java中的深复制和浅复制?
什么是影子clone?
class UnCloneA {
private int i;
public UnCloneA(int ii) {
i = ii;
}
public void doublevalue() {
i *= 2;
}
public String toString() {
return Integer.toString(i);
}
}
class CloneB implements Cloneable {
public int aInt;
public UnCloneA unCA = new UnCloneA(111);
public Object clone() {
CloneB o = null;
try {
o = (CloneB) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class ObjRef {
public static void main(String[] a) {
CloneB b1 = new CloneB();
b1.aInt = 11;
System.out.println("before clone,b1.aInt = " + b1.aInt);
System.out.println("before clone,b1.unCA = " + b1.unCA);
CloneB b2 = (CloneB) b1.clone();
b2.aInt = 22;
b2.unCA.doublevalue();
System.out.println("=================================");
System.out.println("after clone,b1.aInt = " + b1.aInt);
System.out.println("after clone,b1.unCA = " + b1.unCA);
System.out.println("=================================");
System.out.println("after clone,b2.aInt = " + b2.aInt);
System.out.println("after clone,b2.unCA = " + b2.unCA);
}
}
原样拷贝原始对象中的内容。
对基本数据类型,这样的操作是没有问题的,但对非基本类型变量,我们知道它们保存的仅仅是对象的引用,这也导致clone后的非基本类型变量和原始对象中相应的变量指向的是同一个对象。
深拷贝的改进方法:改成深度clone很简单,需要两个改变:一是让UnCloneA类也实现和CloneB类一样的clone功能(实现Cloneable接口,重载clone()方法)。
二是在CloneB的clone()方法中加入一句o.unCA = (UnCloneA)unCA.clone();
注意:要知道不是所有的类都能实现深度clone的,StringBuffer没有重载clone()方法,
更为严重的是StringBuffer还是一个final类,这也是说我们也不能用继承的办法间接实现StringBuffer的clone。
●我们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设
是SringBuffer对象,而且变量名仍为
unCA): o.unCA = new StringBuffer(unCA.toString());
●String对象是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个
仍然是引用,而且都指向了同一个String对象。
但在执行c2.str = c2.str.substring(0,5)的时候,
它作用相当于生成了一个新的String类型,然后又赋回给c2.str。
这是因为String被Sun公司的工程师写成了一个不可更改的类(immutable class),在所有String类中的函数都不能更改自身的值。
➢5. Java的四种引用,强弱软虚,用到的场景。
四种引用类型包括:强引用、弱引用、软引用、虚引用(幽灵引用),使程序能够更灵活的控制对象的生命周期。
1.强引用(StrongReference):强引用不会被垃圾回收器回收,并且也没有实际的对应类
型,平时接触最多的就是强引用。
例如Object obj = new Object();这里obj即为强引用。
如果一个对象具有强引用,垃圾回收器绝不会回收它。
当内存空间不足,java 虚拟机宁愿抛出out of MemoryError错误,是程序异常终止,也不会靠回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)如果一个对象只具有软引用,如果内存空间足够,垃圾回收器
就不会回收它,如果内存空间不足,就会回收这些对象的内存。
只要垃圾回收器没有回收它,该对象就可以被程序使用。
软引用可以用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回
3.弱引用:如果一个对象只具有弱引用,类似于可有可无的生活用品。
弱引用和软引用的
区别:只具有弱引用的对象拥有更短的生命周期。
在垃圾回收器线程扫描他所管辖的内存区域过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回
收它的内存。
由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference):“虚引用”顾名思义,就是形同虚设,与其他几种引用都
不同,虚引用并不会决定对象的生命周期。
如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。
当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。
如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
GC一但发现了虚引用对象,将会将PhantomReference对象插入ReferenceQueue队列. * 而此时PhantomReference所指向的对象并没有被GC回
收,而是要等到ReferenceQueue被你真正的处理后才会被回收
●对象可及性的判断
在很多时候,一个对象并不是从根集直接引用的,而是一个对象被其他对象引用,甚至同时被几个对象所引用,从而构成一个以根集为顶的树形结构。
如图2所示在这个树形的引用链中,箭头的方向代表了引用的方向,所指向的对象是被引用对象。
由图可以看出,从根集到一个对象可以由很多条路径。
比如到达对象5的路径就有①-⑤,③-⑦两条路径。
由此带来了一个问题,那就是某个对象的可及性如何判断:
◆单条引用路径可及性判断:在这条路径中,最弱的一个引用决定对象的可及性。
◆多条引用路径可及性判断:几条路径中,最强的一条的引用决定对象的可及性。
比如,我们假设图2中引用①和③为强引用,⑤为软引用,⑦为弱引用,对于对象5按照这两个判断原则,路径①-⑤取最弱的引用⑤,因此该路径对对象5的引用为软引用。
同样,③-⑦为弱引用。
在这两条路径之间取最强的引用,于是对象5是一个软可及对象。
➢6. Hashcode的作用。
Java中的集合(Collection)有两类,一类是List,再有一类是Set。
前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。
那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是Object.equals方法了。
但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。
也就是说,如果集合中现在已经有1000个元素,
那么第1001个元素加入集合时,它就要调用1000次equals方法。
这显然会大大降低效率。
于是,Java采用了哈希表的原理。
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。
初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
所以这里存在一个冲突解决的问题。
这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
a)hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode
是用来在散列存储结构中确定对象的存储地址的;
b)如果两个对象相同,就是适用于equals(ng.Object) 方法,那么这两个对象的
hashCode一定要相同;
c)如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode
使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;d)两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于
equals(ng.Object) 方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。
➢7. ArrayList、LinkedList、Vector的区别。
●List接口下的实现类:
●ArrayList:
List 接口的大小可变数组的实现,实现了所有可选列表操作,并允许包括null 在内的所有元素。
除了实现List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。
(此类大致上等同于Vector 类,除了此类是不同步的。
)size、isEmpty、get、set、iterator 和listIterator 操作都以固定时间运行。
add 操作以分摊的固定时间运行,也就是说,添加n 个元素需要O(n) 时间。
其他所有操作都以线性时间运行(大体上讲)。
与用于LinkedList 实现的常数因子相比,此实现的常数因子较低。
每个ArrayList 实例都有一个容量。
该容量是指用来存储列表元素的数组的大小。
它总是至少等于列表的大小。
在添加大量元素前,应用程序可以使用ensureCapacity 操作来增加ArrayList 实例的容量。
这可以减少递增式再分配的数量。
此实现不是同步的。
如果多个线程同时访问一个ArrayList 实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。
(这一般通过对自然封装该列表的对象进行同步操作来完成。
如果不存在这样的对象,则应该使用Collections.synchronizedList 方法将该列表“包装”起来。
这最好在创建时完成,以防止意外对列表进行不同步的访问:List list = Collections.synchronizedList(new ArrayList(...));
此类的iterator 和listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的remove 或add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出ConcurrentModificationException。
因此,面对并发的修改,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
●LinkedList:
List 接口的链接列表实现。
实现所有可选的列表操作,并且允许所有元素(包括null)。
除
了实现List 接口外,LinkedList 类还为在列表的开头及结尾get、remove 和insert 元素提供了统一的命名方法。
这些操作允许将链接列表用作堆栈、队列或双端队列。
此类实现Deque 接口,为add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。
所有操作都是按照双重链接列表的需要执行的。
在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。
使用Collections.synchronizedList 方法将该列表“包装”起来。
这最好在创建时完成,以防止意外对列表进行不同步的访问:
List list = Collections.synchronizedList(new ArrayList(...));
(同上)
方法中的add、get、remove、offer、poll、peek都是搭配first、last成对出现的。
●Vector:
Vector 类可以实现可增长的对象数组。
与数组一样,它包含可以使用整数索引进行访问的组件。
但是,Vector 的大小可以根据需要增大或缩小,以适应创建Vector 后进行添加或移除项的操作。
每个向量会试图通过维护capacity 和capacityIncrement 来优化存储管理。
capacity 始终至少应与向量的大小相等;这个值通常比后者大些,因为随着将组件添加到向量中,其存储将按capacityIncrement 的大小增加存储块。
应用程序可以在插入大量组件前增加向量的容量;这样就减少了增加的重分配的量。
线程同步。
(其他同上)
➢8. String、StringBuffer与StringBuilder的区别。
●String 是不可变的对象, 因此在每次对String 类型进行改变的时候其实都等同于生成
了一个新的String 对象,然后将指针指向新的String 对象,所以经常改变内容的字符串最好不要用String ,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM 的GC 就会开始工作,那速度是一定会相当慢的。
●StringBuffer 类则结果就不一样了,每次结果都会对StringBuffer 对象本身进行操作,
而不是生成新的对象,再改变对象引用。
所以在一般情况下我们推荐使用StringBuffer ,特别是字符串对象经常改变的情况下。
●ng.StringBuffer线程安全的可变字符序列。
一个类似于String 的字符串缓冲区,
但不能修改。
虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。
可将字符串缓冲区安全地用于多个线程。
可以在必要时对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
●ng.StringBuilder一个可变的字符序列是5.0新增的。
此类提供一个与StringBuffer
兼容的API,但不保证同步。
该类被设计用作StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。
如果可能,建议优先采用该类,因为在大多数实现中,它比StringBuffer 要快。
两者的方法基本相同。
执行速度逐渐变快,StringBuilder最快。
➢9. Map、Set、List、Queue、Stack的特点与用法。
1)Map
Map中存储的是key-value组成的键值对,key是唯一的,value可以不唯一。
同一个Map对象的任何两个key通过equals方法比较结果总是返回false
TreeMap 是有序的Map(存储的对象需要实现Comparable接口和其中的compareTo
2)List
存储有序的可重复的集合。
可以在任何位置添加和删除元素。
可以用Iterator实现单向遍历,也可以使用ListIterator实现双向遍历(LinkedList)。
ArrayList、vector、LinkedList(实现Deque接口)
3)Set
不包含重复的元素的集合,set中最多包含一个null元素,
只能用Iterator实现单向遍历,Set中没有同步的方法。
HashSet (LinkedHashSet 实现了看似按先后顺序存储) TreeSet 是有序的实现Comparable接口。
4)Queue
遵循先进先出的原则。
(LinkedList实现了Queue,Deque)
使用时尽量避免add\remove方法,用offer\poll方法来添加和移除元素,后两种方法的优点是可以通过返回值判断操作是否成功。
通常不允许插入null;
PriorityQueue并不是一个比较标准的队列实现,PriorityQueue保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序,这点从它的类名也可以看出来。
其中保存的对象同TreeSet类似,要实现Comparable接口和相应的compareTo方法,为了比较大小。
5)Stack
Stack遵从后进先出原则。
Stack继承自Vector。
它通过五个操作对类Vector进行扩展,允许将向量视为堆栈,它提供了通常的push和pop 操作,以及取堆栈顶点的peek()方法、测试堆栈是否为空的empty方法等
➢10. HashMap和HashTable的区别。
●Hashmap是一个数组和链表的结合体(在数据结构称“链表散列“),如下图示:
当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。
如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。
●HashTable和HashMap区别
第一,继承不同。
public class Hashtable extends Dictionary implements Map
public class HashMap extends AbstractMap implements Map
第二
Hashtable 中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。
在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
第三
Hashtable中,key和value都不允许出现null值。
在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
当get()方法返回null值时,即可以表示HashMap中没有该键,也可以表示该键所对应的值为null。
因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。
第四,两个遍历方式的内部实现上不同。
Hashtable、HashMap都使用了Iterator。
而由于历史原因,Hashtable还使用了Enumeration的方式。
第五
哈希值的使用不同,HashTable直接使用对象的hashCode。
而HashMap重新计算hash 值。
第六
Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。
HashTable中hash数组默认大小是11,增加的方式是old*2+1。
HashMap中hash数组的默认大小是16,而且一定是2的指数。
➢11. HashMap和ConcurrentHashMap的区别,HashMap 的底层源码。
➢12. TreeMap、HashMap、LindedHashMap的区别。
✓Hashmap 是一个最常用的Map,它根据键的HashCode 值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。
HashMap 最多只允许一条记录的键为Null;允许多条记录的值为Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。
如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。
✓LinkedHashMap保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。
在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。
✓TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator 遍历TreeMap时,得到的记录是排过序的。
➢13. Collection包结构,与Collections的区别。
Collection 是单列集合(Map是双列集合),是集合类的上级接口。
Collections 是针对集合类的一个帮助类,提供了操作集合的工具方法:一系列静态方法实
现对各种集合的搜索、排序、线程安全化的操作。
此类不能实例化,就像一个工具类
➢14. try catch finally,try里有return,finally还执行么?
a)不管有木有出现异常,finally块中代码都会执行;
b)当try和catch中有return时,finally仍然会执行;
c)finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把
要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
d)finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的
返回值。
注意:finally中对变量值得修改不会影响返回值;
在try语句中,在执行return语句时,要返回的结果已经准备好了,就在此时,程序转到finally 执行了。
在转去之前,try中先把要返回的结果存放到不同于x的局部变量中去,执行完finally之后,在从中取出返回结果,
因此,即使finally中对变量x进行了改变,但是不会影响返回结果。
它应该使用栈保存返回值。
➢15. Excption与Error包结构。
OOM你遇到过哪些情况,SOF你遇到过哪些情况。
OOM: OUT OF MEMORY
SOF:Stack overflow
➢16. Java面向对象的三个特征与含义。
封装可以隐藏实现细节,是代码模块化;
继承可以扩展已存在的代码模块(类);
他们的目的都是代码重用。
多态是为了实现另一个目的:接口重用。
✧封装
什么是封装?封装又叫隐藏实现,只公开代码单元的对外接口,而隐藏其具体实现。
也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
如何实现?在程序设计里,封装通过访问控制实现。
通过访问控制符,用public把信息暴露,private,protected将信息隐藏,来实现封装。
(对代码访问控制的越严格,对代码修改的自由度就越大。
)
封装好处?
1)封装将代码分成了一个个相对独立的单元,使得对代码的修改更加安全和容易。
明
确的指出哪些属性和方法是外部可以访问的,当代吗需要调整时,只要保证共有的
属性不变,共有参数和返回类型不变,就可以自由的修改代码。
2)封装使得整个软件开发复杂度降低。
可以容易的使用别人编写的代码,不必关心内部实现。
3)封装避免了命名冲突问题。
封装有隔离的作用,不同类中有同名的方法和属性,不会混淆。
✧继承:核心思想是重用现有的代码,并用一些已有的类创建新的类。
1)什么是继承?指可以让一个对象获得另一个对象的属性和方法。
继承类可以使用现有类的所有功能,不需要重新编写,并且对这些功能进行扩展。
通过继承创建的新
类成为“子类”“派生类”,被继承的类称为“基类”“父类”。
继承的过程是一般到特殊的
过程。
2)如何实现?
实现继承和接口继承;实现继承可以直接使用基类的属性和方法无需额外的编码。
接口继承指使用属性和方法名,需要自己实现方法。
3)复合和继承,何时使用?
复合,将各个部分组合在一起,程序设计实现时用已有类的对象产生新的类。
a)需要向上转型时考虑继承
b)用“has a”“is a”区分复合和继承。
✧多态
1)什么是多态?指允许不同类的对象对同一消息作出相应。
即同一消息可以根据发送对象的不同而采用多种不同的行为方式。
(发送消息就是函数调用)2)多态存在的三个必要条件:
a)要有继承
b)要有重写
父类引用指向子类对象:1.该引用只能调用父类中定义的方法和变量;2.如果子类中重写了父类中的一个方法,那么调用时将会调用子类中的方法(动态链接、动态调用)
➢20. java多态的实现原理。
Java多态概述:
多态是面向对象元的重要特性,它允许基类的指针或引用指向派生类的对象,在具体访问时实现方法的动态绑定。
Java对于方法的动态绑定的实现主要依赖于方法表,分为类引用调用和方法引用调用。
●类引用调用过程:java编译器将java源代码编译成class文件,编译过程中,会根据静
态类型将调用的符号引用写到class文件中。
运行时,虚拟机根据class文件找到调用方法的符号引用,然后在静态类型方法表中找到偏移量,根据this指针确定对象的实际类型,使用实际类型方法表(偏移量和静态类型中的偏移量一样),如果实际类型的方法表中找到该方法,直接调用,否则认为没有重写父类该方法,按照继承关系从下往上搜索。
●JVM结构
当程序运行时,需要某个类,类载入子系统将相应class文件载入JVM,并建立该类的类型信息(class文件在JVM中的存储结构),包括java类定义所有信息,以及方法表。
这个类型信息存储在方法区。
注意:方法区中的类型信息和堆中存放的java对象不同。
方法去中class的类型信息只有唯一的实例(线程共享),堆中可以有多个对象。
可以通过堆中的class对象访问方法区中的信息类型。
✧方法表是实现动态调用的核心。
为了优化调用速度,方法区中的类型信息会增加一
个指针,指向记录该类方法的方法表,方法表中的每项都对应方法指针。
运行时常量池是方法区的一部分,用于存放编译器生成的符号引用。
●方法表和方法调用
如果子类改写了父类的方法,那么子类和父类的那些同名的方法共享一个方法表项。
所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。
(1)在常量池(这里有个错误,上图为ClassReference常量池而非Party的常量池)中找到方法调用的符号引用。
(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
(3)根据this指针得到具体的对象(即 girl 所指向的位于堆中的对象)。
(4)根据对象得到该对象对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用(Girl的方法表的speak项指向自身的方法而非父类);如果没有重写,则需要拿到按照
继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。
接口调用
因为 Java 类是可以同时实现多个接口的,而当用接口引用调用某个方法的时候,情况就有所不同了。