由于文章可能过长,看自己需要的部分即可…我尽量把java面向对象讲得全面一点,尽量让有一点点Java学习基础的同学也能看得懂,了解的深度也足够.
若在文章中发现了什么问题或者不太理解的地方…请及时评论,我将在评论中补充,谢谢…希望本文能对大家理解Java面向对象知识和底层有一定的了解
面向对象三大主要特征:
封装
将具体的功能封装到方法之中,将对象属性和行为封装到类之中
利用权限隐藏了一些细节信息
继承
事物之间的关系的一种表达,通过继承来表达多种事物之间的子与父的关系
主要是子类继承父类
多态
一个事物在不同环境下表现出来的不同表现形式
主要是通过重载和重写的方式,同一个类的不同对象或者不同子类及子类对象,在执行同一个方法时执行不同的具体代码逻辑
如何写一个JAVA类??
通过访问修饰符+class+类名的方式创建一个java类
而且,由public修饰的类的类名将作为当前.java文件的文件名
一个类中,有两个成员部分,一个是属性,一个是行为
所谓行为,就是方法,不管是对属性的操作亦或是对其他逻辑的处理,均称之为行为,也即方法(method)
类就是对象的模板,描述了对象的属性和行为,类刻画了对象应该是个什么样子
权限访问修饰符
根据封装的含义,我可以将某个类的属性和行为添加访问权限修饰符
private
私有的,顾名思义,只有当前类自己能访问,别的一概不行,不管是同包还是继承,反正你就是不能直接访问到
default
默认的(或者叫缺省的,反正都是英文翻译过来),也就是说,在上一个的基础上,允许在同一个package下访问,同包的子类也可以访问,但是不同包的子类及其子类对象就不能访问父类的这个属性
protected
受保护的,这个受保护的就是在default基础上,允许子类及子类对象能直接访问到
public
全部公开,不管是方法还是属性都能直接调用
根据以上信息,可以写一个类,分别用不同的访问修饰符去修饰每个属性和方法:
package com.a;
public class A {
private int privateValue = 1;
int defaultValue = 2;
protected int protectedValue = 3;
public int publicValue = 4;
public void publicFunc() {
System.out.println("66666");
}
protected void protectedFunc() {
System.out.println("55555");
}
void defaultFunc() {
System.out.println("44444");
}
private void privateFunc() {
System.out.println("33333");
}
}
private
在B类中创建A类的对象,并用A的对象去访问这些属性值
package com.a;
public class B {
public void interview() {
A a = new A();
int aDefault = a.defaultValue;
int aProtected = a.protectedValue;
int aPublic = a.publicValue;
int aPrivate = a.privateValue;// no
a.publicFunc();
a.protectedFunc();
a.defaultFunc();
a.privateFunc(); //no
}
}
编译B类,报出如下错误:
因此说明,在同一个包下的去访问private修饰的属性和方法会导致编译失败,编译都不通过,那就更别说运行了.
default和protected
在同一个包下创建C类,并继承于A类
package com.a;
public class C extends A{
public void interview(){
A a = new A();
int defaultValue = a.defaultValue;
int protectedValue = a.protectedValue;
int publicValue = a.publicValue;
int privateValue = a.privateValue;// no
a.defaultFunc();
a.publicFunc();
a.protectedFunc();
a.privateFunc(); //no
}
}
在另一个包下导入A类,创建D类,并继承于A类
package com.xjk;
import com.a.A;
public class D extends A {
public void interview(){
A a = new A();
int defaultValue = a.defaultValue;//no
int protectedValue = a.protectedValue;//no
int publicValue = a.publicValue;
int privateValue = a.privateValue;// no
a.defaultFunc();//no
a.publicFunc();
a.protectedFunc();//no
a.privateFunc(); //no
}
}
在另外一个包下导入A类,创建E类
package com.xjk;
import com.a.A;
public class E {
public void interview() {
A a = new A();
int aDefault = a.defaultValue;//no
int aProtected = a.protectedValue; //no
int aPublic = a.publicValue;
int aPrivate = a.privateValue;//no
a.publicFunc();
a.protectedFunc();//no
a.defaultFunc();//no
a.privateFunc(); //no
}
}
至此,权限修饰符的问题应该有了一个相对比较清晰的认识
构造函数
构造函数,什么是构造函数?是创建当前对象的函数吗?
Java的构造函数,也就是构造方法,属于Java面向对象中的一种与类名相同且没有返回值的特殊函数,一般用来初始化成员属性和成员方法
而构造函数又存在无参构造和有参构造
无参构造一般都会在默认情况下由编译器自动给你补上,但是你一旦写了有参构造而不写无参构造的话,将不会自动给你添加无参构造方法
有参构造一般用于给属性进行值的设置
仔细理解方法的含义,在构造函数中可以添加自己的相应逻辑,以影响编译器对具体对象的初始化
public class Te{
public int a;
public Te(){
}
public Te(int num){
a=num;
}
}
public class Test{
public static void main(String[] args){
new Te();
}
}
构造函数的执行在对象创建出来之后,用于给对象进行初始化用,可以理解为,在new的时候只是创建,而在()的时候,将对象进行初始化.
new 关键字
new关键字主要是用于创建对象,并调用对象的构造方法进行初始化
JAVA中对象是属于reference数据类型,而Java中reference数据类型一般都是在Java堆中开辟空间
我们将上面的Test类编译后反编译查看main方法字节码
其中会发现有一行指令叫做new,通过java8虚拟机编码规范中可以知道,new关键字是在堆中开辟空间
并且在这个文档之中对new指令有一个note:
指令不会完全创建新实例;等到在未初始化的实例上调用了实例初始化方法之后,才会完成实例创建。
this 关键字
this理解为当前的,不论是当前的类还是当前的对象,this在类编写的代码中出现,在当前类之中互相调用,比如方法调用当前这个里面的属性,或者调用当前的方法
用this测试一下属性
public class Te {
public int a;
public Te(){
}
public void testThis(){
this.a = 10;
}
}
这里的this就是去找到了当前对象的a属性
字节码层面,你会看到执行了一个 putfield 指令(设置对象中的字段) 这个时候就是将10这个值赋值到了当前的对象a属性值中
用this测试一下方法
public class Te {
public int a;
public Te(){
}
public void ttt(){
}
public void testThis(){
this.ttt();
}
}
这里的this就是去找到了当前对象的ttt方法
字节码层面,出现了一个invokevirtual指令(调用实例方法,基于类对象的调度)
JAVA类的详细分析
首先,需要知道Java Memory Model可以简单地理解为主要分三个部分(当然,实际上区域划分会很多,暂时先可以这么理解):
Java虚拟机栈
Java堆数据区
Method Area
先举个例子:
public class Te {
public int a;
public Te() {
}
}
public class Test {
public static void main(String[] args) {
Te t = new Te();
}
}
Te和Test两个类的字节码分别如下:
Classfile /H:/LEETCODE/exercise/out/production/works/Te.class
Last modified 2022-7-7; size 251 bytes
MD5 checksum d619c5617c569341a22c4eb7a782b998
Compiled from "Te.java"
public class Te
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // Te
#3 = Class #17 // java/lang/Object
#4 = Utf8 a
#5 = Utf8 I
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 LTe;
#13 = Utf8 SourceFile
#14 = Utf8 Te.java
#15 = NameAndType #6:#7 // "<init>":()V
#16 = Utf8 Te
#17 = Utf8 java/lang/Object
{
public int a;
descriptor: I
flags: ACC_PUBLIC
public Te();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
line 6: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTe;
}
SourceFile: "Te.java"
Classfile /H:/LEETCODE/exercise/out/production/works/Test.class
Last modified 2022-7-7; size 401 bytes
MD5 checksum fad05a93abe8c01bc98509de16ffdc03
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // Te
#3 = Methodref #2.#21 // Te."<init>":()V
#4 = Class #23 // Test
#5 = Class #24 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 LTest;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 t
#18 = Utf8 LTe;
#19 = Utf8 SourceFile
#20 = Utf8 Test.java
#21 = NameAndType #6:#7 // "<init>":()V
#22 = Utf8 Te
#23 = Utf8 Test
#24 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Te
3: dup
4: invokespecial #3 // Method Te."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 4: 0
line 5: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
8 1 1 t LTe;
}
SourceFile: "Test.java"
首先肯定需要吧Java虚拟机启动起来吧,不然你字节码在哪儿运行呢??
既然Java虚拟机都起来了,那是不是那些类加载器啥的也应该也在虚拟机里存在了吧
那么类加载器到底该怎么加载你的这几个类的呢??
通过ClassLoader执行loadClass方法加载当前你执行的第一个main方法中的那个类,也即(Test类)
此时,会在堆中开辟出Test类的区域,Te类的区域
当main方法执行到new时创建出对象的空间
…
具体的过程就是如下:(当然,这只是暂时的方便理解,具体的详细过程远远比这个复杂得多)
1.虚拟机使用ClassLoader(类加载器)加载并创建了当前你这个Test.class字节码文件和Te.class字节码文件对应的类,开辟了这个Test.class和Te.class的堆区域
2.通过虚拟机里面的类加载器将Te.class和Test.class中对应的属性附上初始值,并将其对应的方法附上方法区的代码段首地址
3.在栈中开辟空间指向当前main方法所在的,虚拟机加载的类class文件的首地址(也即Test.class的首地址),将值push进栈
4.将Test.class中main方法的地址压栈,开始执行main方法的指令代码
5.当执行main方法到new指令时,会先判断当前的这个类所创建的对象是否是大对象.如果不是大对象,会在堆中的Eden区域开辟空间;如果是大对象,直接会在老年区开辟对象空间(Eden区域和老年区是垃圾回收划分的区域,暂时先知道在堆中有这么两块区域即可)
6.当Te类的对象t被创建之后,栈中会开辟一块空间指向当前被创建出来的t对象的首地址,也就是我们代码中的变量t,之后指令便会走到invokespecial指令处,完成对t对象的初始化,也就是会跳转到Te的构造方法中执行
7.Te的构造方法执行的时候,便会将t中的变量该赋值的赋值,该咋咋滴,当然,在执行任何方法的时候都是在栈中开辟空间进行代码运算
8.结束Te的构造方法后,t的对象也就被完全创建并且初始化完成了,而且此时在栈中指向t的对象地址并没有被销毁,而是在等到main方法执行完成,main在栈中的所有数据清空时,栈中的t对象地址才会被清空
9.由于t对象没有任何引用指向当前的空间,于是便会被垃圾回收器认为已经属于无效垃圾对象,进而清空t对象在堆中的空间
以上便是单独对一个对象的创建具体过程
方法重载
什么叫方法重载?百度的解释为:指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。
划重点: 同名 不同参数类型和个数
看看如下代码:
package com.xjk;
public class G {
public void fun(){
}
public void fun(int a){
}
public void fun(int a,int b){
}
public void fun(int a,float b){
}
public void fun(float a,int b){
}
}
编译,查看这个的字节码
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/G.class
Last modified 2022-7-9; size 652 bytes
MD5 checksum 871d8cb6fdd44a821c853caaafee1d3e
Compiled from "G.java"
public class com.xjk.G
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // com/xjk/G
#3 = Class #24 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 LocalVariableTable
#9 = Utf8 this
#10 = Utf8 Lcom/xjk/G;
#11 = Utf8 fun
#12 = Utf8 (I)V
#13 = Utf8 a
#14 = Utf8 I
#15 = Utf8 (II)V
#16 = Utf8 b
#17 = Utf8 (IF)V
#18 = Utf8 F
#19 = Utf8 (FI)V
#20 = Utf8 SourceFile
#21 = Utf8 G.java
#22 = NameAndType #4:#5 // "<init>":()V
#23 = Utf8 com/xjk/G
#24 = Utf8 java/lang/Object
{
public com.xjk.G();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/xjk/G;
public void fun();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/xjk/G;
public void fun(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 14: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/xjk/G;
0 1 1 a I
public void fun(int, int);
descriptor: (II)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/xjk/G;
0 1 1 a I
0 1 2 b I
public void fun(int, float);
descriptor: (IF)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 22: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/xjk/G;
0 1 1 a I
0 1 2 b F
public void fun(float, int);
descriptor: (FI)V
flags: ACC_PUBLIC
Code:
stack=0, locals=3, args_size=3
0: return
LineNumberTable:
line 25: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/xjk/G;
0 1 1 a F
0 1 2 b I
}
SourceFile: "G.java"
为啥不报错呢?这么多方法名一样的代码,为啥呢?因为Java里存在方法签名
方法签名
很好理解,给每个方法添加一个特殊标识,用来唯一确定方法
看到字节码中,方法名都是同样的,但是descriptor并不相同上面的分别是
()V
无参数,无返回值
(I)V
一个int参数,无返回值
(II)V
两个int参数,无返回值
(IF)V
第一个int类型,第二个float类型,无返回值
(FI)V
第一个float类型,第二个int类型,无返回值
但是参数类型相同,返回类型不同的两个同名方法会报错,这为什么呢?明明方法都有签名,签名不一样为啥还会报错呢?你把你想象成JVM,现在正在运行中,突然给了你两个方法,让你选择一个,这两个方法都一样,唯一不同是返回值不同,那我代码都没有执行完我怎么知道到底该返回啥,那我怎么选择,直接裂开了…
类的继承
在学习继承前,需要先明白一点,所有的JAVA类全部继承于一个叫做Object的父类
其中Every class has {@code Object} as a superclass. 说明了所有类的基类都是Object
extends关键字
这个关键字的翻译成中文意思是扩展,在java语言层面就是一个继承的意思,并且,Java语言不允许多继承,只允许继承1个类,所以继承都是一个一个继承下来的
写个demo了解了解:
先创建出两个类,分别是A,B,并且由B去继承A
public class A {
public int num = 11;
public A() {
}
public A(int num) {
this.num = num;
}
public void fun(){
}
}
public class B extends A{
public int bNum;
public B() {
}
public B(int bNum) {
this.bNum = bNum;
}
}
详细分析继承
对B继承了A的字节码进行分析
Classfile /H:/LEETCODE/exercise/out/production/works/com/a/B.class
Last modified 2022-7-7; size 355 bytes
MD5 checksum db2c707251de3d895d8e53048e04332f
Compiled from "B.java"
public class com.a.B extends com.a.A
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // com/a/A."<init>":()V
#2 = Fieldref #3.#18 // com/a/B.bNum:I
#3 = Class #19 // com/a/B
#4 = Class #20 // com/a/A
#5 = Utf8 bNum
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/a/B;
#14 = Utf8 (I)V
#15 = Utf8 SourceFile
#16 = Utf8 B.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // bNum:I
#19 = Utf8 com/a/B
#20 = Utf8 com/a/A
{
public int bNum;
descriptor: I
flags: ACC_PUBLIC
public com.a.B();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/a/A."<init>":()V
4: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/a/B;
public com.a.B(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method com/a/A."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field bNum:I
9: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/a/B;
0 10 1 bNum I
}
SourceFile: "B.java"
如果B类不继承于A类的话,字节码如下:
Classfile /H:/LEETCODE/exercise/out/production/works/com/a/B.class
Last modified 2022-7-7; size 364 bytes
MD5 checksum b6bd8bff9afb52ae3cadb1d5a5671452
Compiled from "B.java"
public class com.a.B
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#18 // com/a/B.bNum:I
#3 = Class #19 // com/a/B
#4 = Class #20 // java/lang/Object
#5 = Utf8 bNum
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/a/B;
#14 = Utf8 (I)V
#15 = Utf8 SourceFile
#16 = Utf8 B.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // bNum:I
#19 = Utf8 com/a/B
#20 = Utf8 java/lang/Object
{
public int bNum;
descriptor: I
flags: ACC_PUBLIC
public com.a.B();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
line 12: 4
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/a/B;
public com.a.B(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field bNum:I
9: return
LineNumberTable:
line 14: 0
line 15: 4
line 16: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/a/B;
0 10 1 bNum I
}
SourceFile: "B.java"
对比常量池第19和第20行 (#19 #20)说明了除了将本类的路径添加到了常量池,还把其父类的路径添加到了常量池,而如果不继承A类,则直接继承了Object;如果继承了A类,又A继承了Object,所以推理得出B还是同样继承于Object.
那么,类的继承这件事…在JMM中到底是做了一件什么不得了的事情呢??
同样的,咱们先小写一点代码,继承关系…看类名应该能ok
package com.xjk;
public class TestFather {
public int fatherNum1 = 1;
public int fatherNum2;
public TestFather() {
}
public TestFather(int fatherNum1, int fatherNum2) {
this.fatherNum1 = fatherNum1;
this.fatherNum2 = fatherNum2;
}
}
package com.xjk;
public class TestSon extends TestFather{
public int sonNum1 = 2;
public int sonNum2;
public TestSon() {
}
public TestSon(int sonNum1, int sonNum2) {
this.sonNum1 = sonNum1;
this.sonNum2 = sonNum2;
}
}
通过HSDB可以知道,在创建自己的本类对象前,一定会在虚拟机中创建出了一些虚拟机需要用到或者加载当前类所需要用到的一些类、对象或者一些其他的数据信息,这通过多方面说明了一件事,在Java虚拟机创建之后,也就是你自己的对象在创建之前,就有了很多的一些信息已经被虚拟机创建了,那么我们作为一个Java开发人员,我们就可以暂时先不去考虑类对象的创建
再把这两个的字节码搬出来
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/TestFather.class
Last modified 2022-7-9; size 470 bytes
MD5 checksum 9e1dce0784fc5bf12e82965132fcb80c
Compiled from "TestFather.java"
public class com.xjk.TestFather
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#19 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#20 // com/xjk/TestFather.fatherNum1:I
#3 = Fieldref #4.#21 // com/xjk/TestFather.fatherNum2:I
#4 = Class #22 // com/xjk/TestFather
#5 = Class #23 // java/lang/Object
#6 = Utf8 fatherNum1
#7 = Utf8 I
#8 = Utf8 fatherNum2
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/xjk/TestFather;
#16 = Utf8 (II)V
#17 = Utf8 SourceFile
#18 = Utf8 TestFather.java
#19 = NameAndType #9:#10 // "<init>":()V
#20 = NameAndType #6:#7 // fatherNum1:I
#21 = NameAndType #8:#7 // fatherNum2:I
#22 = Utf8 com/xjk/TestFather
#23 = Utf8 java/lang/Object
{
public int fatherNum1;
descriptor: I
flags: ACC_PUBLIC
public int fatherNum2;
descriptor: I
flags: ACC_PUBLIC
public com.xjk.TestFather();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field fatherNum1:I
9: return
LineNumberTable:
line 11: 0
line 8: 4
line 12: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/xjk/TestFather;
public com.xjk.TestFather(int, int);
descriptor: (II)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field fatherNum1:I
9: aload_0
10: iload_1
11: putfield #2 // Field fatherNum1:I
14: aload_0
15: iload_2
16: putfield #3 // Field fatherNum2:I
19: return
LineNumberTable:
line 14: 0
line 8: 4
line 15: 9
line 16: 14
line 17: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/xjk/TestFather;
0 20 1 fatherNum1 I
0 20 2 fatherNum2 I
}
SourceFile: "TestFather.java"
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/TestSon.class
Last modified 2022-7-9; size 457 bytes
MD5 checksum 8b8f4f3ead58b12058781b02bc7b350e
Compiled from "TestSon.java"
public class com.xjk.TestSon extends com.xjk.TestFather
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#19 // com/xjk/TestFather."<init>":()V
#2 = Fieldref #4.#20 // com/xjk/TestSon.sonNum1:I
#3 = Fieldref #4.#21 // com/xjk/TestSon.sonNum2:I
#4 = Class #22 // com/xjk/TestSon
#5 = Class #23 // com/xjk/TestFather
#6 = Utf8 sonNum1
#7 = Utf8 I
#8 = Utf8 sonNum2
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/xjk/TestSon;
#16 = Utf8 (II)V
#17 = Utf8 SourceFile
#18 = Utf8 TestSon.java
#19 = NameAndType #9:#10 // "<init>":()V
#20 = NameAndType #6:#7 // sonNum1:I
#21 = NameAndType #8:#7 // sonNum2:I
#22 = Utf8 com/xjk/TestSon
#23 = Utf8 com/xjk/TestFather
{
public int sonNum1;
descriptor: I
flags: ACC_PUBLIC
public int sonNum2;
descriptor: I
flags: ACC_PUBLIC
public com.xjk.TestSon();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/xjk/TestFather."<init>":()V
4: aload_0
5: iconst_2
6: putfield #2 // Field sonNum1:I
9: return
LineNumberTable:
line 11: 0
line 8: 4
line 12: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/xjk/TestSon;
public com.xjk.TestSon(int, int);
descriptor: (II)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method com/xjk/TestFather."<init>":()V
4: aload_0
5: iconst_2
6: putfield #2 // Field sonNum1:I
9: aload_0
10: iload_1
11: putfield #2 // Field sonNum1:I
14: aload_0
15: iload_2
16: putfield #3 // Field sonNum2:I
19: return
LineNumberTable:
line 14: 0
line 8: 4
line 15: 9
line 16: 14
line 17: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this Lcom/xjk/TestSon;
0 20 1 sonNum1 I
0 20 2 sonNum2 I
}
SourceFile: "TestSon.java"
可以观察到,子类的字节码中有两个方法,这两个方法是将子类的构造方法编译成字节码指令后的结果,并且能够看到,这两个字节码均有一个指令叫做invokespecial,并且还有一个操作数来自于常量池,而且根据注释可以看到,这个值指向了TestFather的<init>方法也就是初始化方法也即构造方法
那么说明在创建子类对象的时候,同时执行了父类的构造方法,并且是父类的无参构造方法
那也就说明,父类的对象也被创建出来的了,此时…思考一个问题…既然是默认执行的是父类的无参构造方法,那我要是想执行的是父类的有参构造方法怎么办?我想要给父类的对象的某个属性赋值怎么办??
于是,引出了一个关键字:super
super关键字
正如上文所说到,需要子类对象创建需要执行父类的构造方法,那么我想指定父类的某个构造方法(因为构造方法不唯一嘛,有无参的,有有参的)创建并初始化父类的具体的对象
修改TestSon这个类:
package com.xjk;
public class TestSon extends TestFather{
public int sonNum1 = 2;
public int sonNum2;
public TestSon() {
super();
}
public TestSon(int sonNum1, int sonNum2) {
super(sonNum1,sonNum2);
this.sonNum1 = sonNum1;
this.sonNum2 = sonNum2;
}
}
再看一下字节码有什么变化
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/TestSon.class
Last modified 2022-7-9; size 469 bytes
MD5 checksum f00f0f9861ca9b284d09a2b4a43984ca
Compiled from "TestSon.java"
public class com.xjk.TestSon extends com.xjk.TestFather
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // com/xjk/TestFather."<init>":()V
#2 = Fieldref #5.#21 // com/xjk/TestSon.sonNum1:I
#3 = Methodref #6.#22 // com/xjk/TestFather."<init>":(II)V
#4 = Fieldref #5.#23 // com/xjk/TestSon.sonNum2:I
#5 = Class #24 // com/xjk/TestSon
#6 = Class #25 // com/xjk/TestFather
#7 = Utf8 sonNum1
#8 = Utf8 I
#9 = Utf8 sonNum2
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/xjk/TestSon;
#17 = Utf8 (II)V
#18 = Utf8 SourceFile
#19 = Utf8 TestSon.java
#20 = NameAndType #10:#11 // "<init>":()V
#21 = NameAndType #7:#8 // sonNum1:I
#22 = NameAndType #10:#17 // "<init>":(II)V
#23 = NameAndType #9:#8 // sonNum2:I
#24 = Utf8 com/xjk/TestSon
#25 = Utf8 com/xjk/TestFather
{
public int sonNum1;
descriptor: I
flags: ACC_PUBLIC
public int sonNum2;
descriptor: I
flags: ACC_PUBLIC
public com.xjk.TestSon();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/xjk/TestFather."<init>":()V
4: aload_0
5: iconst_2
6: putfield #2 // Field sonNum1:I
9: return
LineNumberTable:
line 12: 0
line 8: 4
line 13: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/xjk/TestSon;
public com.xjk.TestSon(int, int);
descriptor: (II)V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: iload_1
2: iload_2
3: invokespecial #3 // Method com/xjk/TestFather."<init>":(II)V
6: aload_0
7: iconst_2
8: putfield #2 // Field sonNum1:I
11: aload_0
12: iload_1
13: putfield #2 // Field sonNum1:I
16: aload_0
17: iload_2
18: putfield #4 // Field sonNum2:I
21: return
LineNumberTable:
line 16: 0
line 8: 6
line 17: 11
line 18: 16
line 19: 21
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/xjk/TestSon;
0 22 1 sonNum1 I
0 22 2 sonNum2 I
}
SourceFile: "TestSon.java"
TestSon中对应的无参构造方法没什么变化,而由于在其有参构造方法中添加了super(num1,num2);
所以这就导致TestSon的有参构造方法发生了改变,并且仔细观察两个字节码中的invokespecial指令后面的注释,<init>后面的()V
变成了(II)V
这就是说明,确实执行了父类的有参构造方法了,这就是super关键字的作用
方法重写
方法重写,也即方法覆盖,在C语言中不存在方法覆盖,但是在Java语言中存在方法重写
方法重写到底是在干嘛呢?由于父类中存在某个方法,如果子类的需要重新实现怎么办呢??那么就需要将父类的方法在子类中重写,子类的该方法名与父类的相同,返回值相同,参数相同,但是方法体却不一样,具体的代码逻辑不同
还是不太理解?
举个栗子:
鸟禽类有进食的这样一个行为(也就是方法),但是针对于麻雀类,燕子类,大雁类,家禽类…等等子类的具体实现吃的方式都不同,如果不重写这个方法…那是不是所有的鸟类都是同一个进食方式?那还有生物多样性可言?
所以…方法重写是必要的
那么Java中怎么实现方法重写的呢??这个方法重写的到底咋写呢??
Father类和Son类,Father类中有一个eat()
方法,子类也有一个eat()
方法,这就是重写
package com.xjk;
public class Father {
public void eat(){
System.out.println("父亲吃饭,用筷子吃,吧唧吧唧...");
}
}
package com.xjk;
public class Son extends Father{
@Override
public void eat() {
System.out.println("儿子吃饭,用勺子吃,米饭洒落一地...");
}
}
Son的字节码如下:
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/Son.class
Last modified 2022-7-9; size 507 bytes
MD5 checksum 0d65165d75f17cddef234b3f7fd8b326
Compiled from "Son.java"
public class com.xjk.Son extends com.xjk.Father
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#17 // com/xjk/Father."<init>":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // 儿子吃饭,用勺子吃,米饭洒落一地...
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #23 // com/xjk/Son
#6 = Class #24 // com/xjk/Father
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/xjk/Son;
#14 = Utf8 eat
#15 = Utf8 SourceFile
#16 = Utf8 Son.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = Class #25 // java/lang/System
#19 = NameAndType #26:#27 // out:Ljava/io/PrintStream;
#20 = Utf8 儿子吃饭,用勺子吃,米饭洒落一地...
#21 = Class #28 // java/io/PrintStream
#22 = NameAndType #29:#30 // println:(Ljava/lang/String;)V
#23 = Utf8 com/xjk/Son
#24 = Utf8 com/xjk/Father
#25 = Utf8 java/lang/System
#26 = Utf8 out
#27 = Utf8 Ljava/io/PrintStream;
#28 = Utf8 java/io/PrintStream
#29 = Utf8 println
#30 = Utf8 (Ljava/lang/String;)V
{
public com.xjk.Son();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/xjk/Father."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/xjk/Son;
public void eat();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String 儿子吃饭,用勺子吃,米饭洒落一地...
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 10: 0
line 11: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/xjk/Son;
}
SourceFile: "Son.java"
在《阿里巴巴Java开发手册》中规定,任何重写的方法必须添加@Override
注解,但是…你实在不加也不会影响编译运行,只是…极度不规范而已
如果父类没有做任何实现,子类重写最好是做实现…
类的多态
在上面的方法重写中的那个举例…就是多态
多态是同一个行为具有多个不同表现形式或形态的能力。
在了解类的多态前先回忆一下,我们平常的Java代码中+
这个操作符绝对用得不少吧,你有没有想过,这个是不是一个多态呢?+
操作符若是对数值操作,是不是将数据相加?那要是对字符串操作呢?则变成了字符串拼接,对不对?那么这个我们可不可以称之为+
符号的多态?(这个在c++中叫做运算符重载)
想要实现多态,主要需要满足以下两个条件:
- 需要有子类继承父类
- 调用方法要重写
Java面向对象中的两个关系挺好的关键字
除了上面已经讲过的一些关键字之外,还有一些关键字
static
顾名思义,static修饰的就是静态的
能够修饰变量、方法以及代码块
用static修饰的是通过类名直接访问的
怎么理解呢?
在之前类的详细分析中是不是说过jvm会开辟一个class文件的类的空间?而且已经加载进去了初始值了,而且类的初始化由jvm处理了(这里说的初始化不是这个类的对象的初始化,而是这个类的初始化,一定要分清楚!)
那么就可以直接通过类名直接获取到对应的数据了或直接执行方法
package com.xjk;
public class G {
public static int num = 10;
public static void fun(){
}
}
package com.xjk;
public class H {
public static void main(String[] args) {
G.fun(); //类名直接访问方法
G.num; //类名直接访问属性
}
}
final
译为最终的
那理解起来就很简单了,就是最终的了嘛,不能被继承了嘛,断子绝孙的那种嘛
往往与static配合使用,直接通过类名进行访问,一般用在定值上(而且必须在定义的时候就赋值,否则编译报错的哦),比如:
package com.xjk;
public class G {
public static final int FINAL_NUMBER = 100;
public static final void function() {
System.out.println("=====");
}
}
package com.xjk;
public class H {
public static void main(String[] args) {
System.out.println(G.FINAL_NUMBER);
G.function();
}
}
如果不和static配合,那就通过对象获取这个固定值了咯
package com.xjk;
public class G {
public final int FINAL_NUMBER = 100;
public final void function() {
System.out.println("=====");
}
}
package com.xjk;
public class H {
public static void main(String[] args) {
G g = new G();
System.out.println(g.FINAL_NUMBER);
g.function();
}
}
我不信,我偏要继承看看
package com.xjk;
public class I extends G{
}
package com.xjk;
public class H {
public static void main(String[] args) {
I i = new I();
System.out.println(i.FINAL_NUMBER);
i.function();
}
}
编译运行,欸!
通过了居然!不合常理啊
final修饰method
那么,final到底是怎么个"断子绝孙"??
我在子类中重写父类用final修饰的方法,编译
package com.xjk;
public class I extends G{
public void function(){
}
}
报错了,这说明子类不可以重写父类的方法
哦~原来是这么个的意思啊…
final修饰类
那也就是说,如果一个类被final修饰了
package com.xjk;
public final class G {
public final int FINAL_NUMBER = 100;
public final void function() {
System.out.println("=====");
}
}
那么这个类就不可以被继承了…
final修饰属性
final修饰的变量一般情况下属于编译期常量如前面出现过的FINAL_NUMBER
,另外还有一种非编译期常量
如下代码中
package com.xjk;
import java.util.Random;
public class G {
public final int FINAL_NUMBER = 100;
Random rand = new Random();
final int RF = rand.nextInt();
public final void function() {
System.out.println("=====");
}
}
RF变量便是由于Random类在运行时赋值的,并且在初始化后就不能被修改了的
类中的代码块
什么是代码块?用{}
括起来的就叫代码块
方法的代码块
什么方法的代码块,那叫方法体!但是他也是用{}
括起来的,叫他代码块也没错
构造方法代码块
也就是用来初始化对象的代码块呗
普通代码块
又称之为构造代码块,每次对象创建时就会执行的代码块,比构造方法先执行
静态代码块
用static{}包裹起来的代码片段,多次对象创建,也只会执行一次。静态代码块优先于普通代码块的执行。
写个demo测试一下:
来个父类TestCodeBlockA,再来个子类TestCodeBlockBja
package com.block;
public class TestCodeBlockA {
public TestCodeBlockA() {
System.out.println("父类对象的初始化");
}
{
System.out.println("父类的普通代码块");
}
static{
System.out.println("父类的静态代码块");
}
}
package com.block;
public class TestCodeBlockB extends TestCodeBlockA{
public TestCodeBlockB() {
System.out.println("子类的初始化");
}
{
System.out.println("子类的普通代码块");
}
static{
System.out.println("子类的静态代码块");
}
}
Test测试:
package com.block;
public class Test {
public static void main(String[] args) {
new TestCodeBlockB();
}
}
测试结果如下:
ok,根据测试,我们可以得出结论,整个的执行过程应该如下:
对于单个类来讲:
静态代码块 => 普通代码块(构造代码块) => 构造方法
对于存在继承关系的类来讲:
父类的静态代码块 => 子类静态代码块 => 父类构造代码块 => 父类构造方法 => 子类构造代码块 => 子类构造方法
如果要创建多个子类对象:
静态代码块只执行一次,后面的父类构造代码块 => 父类构造方法 => 子类构造代码块 => 子类构造方法根据子类的对象的多少进行重复
抽象类
Java 语言提供了两种类,分别为具体类 和 抽象类
如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类称为抽象类。
什么意思呢?举个栗子:
动物类、飞禽类、东北虎类、节肢动物类、北极熊类、狼蛛类、眼镜蛇类、爬行动物类、麻雀类、百灵鸟类、金丝雀类、山斑鸠类…
那么以上的这几个种类中,哪些是抽象类,哪些是具体类呢??
动物类…这个概念太抽象了吧…到底什么是动物?就如果用动物来表示具体的对象的话…你是动物类的具体对象,邻居家养的那只拉布拉多犬也是动物类的具体对象,牧场中的那只即将待宰的花猪也是动物类具体的对象…那这个动物类表示的范围实在太广了,没办法表示特定的那个类所对应的具体对象
东北虎类…这个对应的对象是什么呢?那肯定就是东北虎嘛,而且也只有东北虎这一种嘛…
这样一看,抽象类与具体类是不是相对比较清晰了呢?
既然了解了抽象类和具体类的含义,那么Java中的抽象类该怎么写呢??
那么需要学习一个修饰符关键字
abstract关键字
同样的,通过字面意思的理解,翻译成中文就是抽象的
有了上面的对抽象概念的理解,那么直接上代码:
package com.xjk.abstr;
public abstract class Animal {
}
package com.xjk.abstr;
public class Dog extends Animal{
}
这个代码…容易理解吧
那么abstract能修饰哪些东西呢??
abstract修饰类
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用。
来,测试一下
package com.xjk.abstr;
public class Test {
public static void main(String[] args) {
new Animal();
}
}
编译报错:
abstract修饰方法
被abstract修饰的方法必须是在抽象类中,不然…就给你编译报错
我怎么理解abstract修饰的方法呢?
抽象的行为:比如 动物吃东西这个行为是抽象的,但是具体到北极熊吃鱼,树袋熊吃树叶…就不一样了,这说明行为也是可以进行抽象的
而且抽象的方法(也就是抽象的行为)没有具体的实现,由继承当前抽象类的具体子类去实现即可
package com.xjk.abstr;
public abstract class Animal {
public abstract void run();
}
package com.xjk.abstr;
public class Dog extends Animal{
@Override
public void run() {
System.out.println("修勾running...");
}
}
package com.xjk.abstr;
public class Test {
public static void main(String[] args) {
new Dog().run();
}
}
结果如下
接口的详细分析
之前全都是class,为什么要引入接口这么个概念呢?
如果所有的抽象方法都得用一个抽象类来写,感觉代码量相对较多,而且一般如果抽象类中没有用到那么些属性,是不是感觉代码冗余量有点多呢??每个抽象方法都要加abstract…作为相对比较懒的人来讲…我是不愿意重复写这些东西的.所以用接口来整合一下.
在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过实现接口的方式,从而来重写接口的抽象方法。
那么…接口怎么写呢??
package com.xjk;
public interface Test {
void function();
}
接口与类的相似点
- 一个接口可以有多个方法。
- 接口文件保存在 .java 结尾的文件中,文件名使用接口名。
- 接口的字节码文件保存在 .class 结尾的文件中。
- 接口相应的字节码文件必须在与包名称相匹配的目录结构中。
接口与类的区别
- 接口不能用于实例化对象。
- 接口没有构造方法。
- 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
- 接口不能包含成员变量,除了 static 和 final 变量。
- 接口不是被类继承了,而是要被类实现。
- 接口支持多继承。
接口与抽象类的区别
- 抽象类中的方法可以有方法体,就是能实现方法的具体功能,但是接口中的方法不行。
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
- 接口中不能含有静态代码块以及静态方法(用 static 修饰的方法),而抽象类是可以有静态代码块和静态方法
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
注:JDK 1.8 以后,接口里可以有静态方法和方法体了。
注:JDK 1.8 以后,接口允许包含具体实现的方法,该方法称为"默认方法",默认方法使用 default 关键字修饰。
注:JDK 1.9 以后,允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。
接口的多继承
package com.xjk;
public interface InterfaceA {
void funA();
}
package com.xjk;
public interface InterfaceB {
void funB();
}
package com.xjk;
public interface InterfaceC extends InterfaceA,InterfaceB{
public static final int a = 10;
public static void test(){
System.out.println("InterfaceC的静态方法,直接通过接口名调用,和类一样");
}
default void funC(){
System.out.println("InterfaceC的默认方法");
}
void funCC();
}
implements关键字
InterfaceC继承了InterfaceA,InterfaceB
下面写一个测试类,用这个类来实现InterfaceC接口
package com.xjk;
public class Test implements InterfaceC{
@Override
public void funA() {
}
@Override
public void funB() {
}
@Override
public void funCC() {
}
}
这个时候你会发现,我只实现InterfaceC接口,但是InterfaceA和InterfaceB的方法我也得实现
并且还有一点,被default修饰的funC方法也可以重写进行实现
interface的字节码分析
对InterfaceC进行字节码分析:
Classfile /H:/JVM-JUC/JVM-Stu/out/production/JVM-Stu/com/xjk/InterfaceC.class
Last modified 2022-7-9; size 672 bytes
MD5 checksum 1cad71a3676da692df40782790f6fb09
Compiled from "InterfaceC.java"
public interface com.xjk.InterfaceC extends com.xjk.InterfaceA,com.xjk.InterfaceB
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#2 = String #26 // InterfaceC的静态方法,直接通过接口名调用,和类一样
#3 = Methodref #27.#28 // java/io/PrintStream.println:(Ljava/lang/String;)V
#4 = String #29 // InterfaceC的默认方法
#5 = Class #30 // com/xjk/InterfaceC
#6 = Class #31 // java/lang/Object
#7 = Class #32 // com/xjk/InterfaceA
#8 = Class #33 // com/xjk/InterfaceB
#9 = Utf8 a
#10 = Utf8 I
#11 = Utf8 ConstantValue
#12 = Integer 10
#13 = Utf8 test
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 funC
#18 = Utf8 LocalVariableTable
#19 = Utf8 this
#20 = Utf8 Lcom/xjk/InterfaceC;
#21 = Utf8 funCC
#22 = Utf8 SourceFile
#23 = Utf8 InterfaceC.java
#24 = Class #34 // java/lang/System
#25 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#26 = Utf8 InterfaceC的静态方法,直接通过接口名调用,和类一样
#27 = Class #37 // java/io/PrintStream
#28 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#29 = Utf8 InterfaceC的默认方法
#30 = Utf8 com/xjk/InterfaceC
#31 = Utf8 java/lang/Object
#32 = Utf8 com/xjk/InterfaceA
#33 = Utf8 com/xjk/InterfaceB
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
{
public static final int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 10
public static void test();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #1 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #2 // String InterfaceC的静态方法,直接通过接口名调用,和类一样
5: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
line 9: 8
public void funC();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #1 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String InterfaceC的默认方法
5: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 12: 0
line 13: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/xjk/InterfaceC;
public abstract void funCC();
descriptor: ()V
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "InterfaceC.java"
对于InterfaceC中的funCC方法,由于没有方法体,所以字节码中没有Code区域
而且,funCC在编写代码的时候并没有添加任何修饰符,但从字节码来看,flags: ACC_PUBLIC, ACC_ABSTRACT
也就是说明了默认在interface中的接口方法都是由public abstract修饰的
内部类
成员内部类
最普通的内部类,就是在另一个类的内部,也就是外部类中的成员部分
public class Outer {
public static int anInt = 100;
public int anInt1 = 200;
class InnerClass{
public void print(){
System.out.println(anInt1);
}
}
}
也就是说成员内部类能够访问到外部那个类的属性,能够访问到外部类的所有 方法和属性
局部内部类
定义在一个方法中或者在一个代码块中的类
public class Outer {
public static int anInt = 100;
public int anInt1 = 200;
public void test(){
int a= 10;
class LocalInnerClass{
public void tt(){
System.out.println(anInt1);
System.out.println(a);
}
}
}
}
在栈中开辟空间,随着方法从栈中弹出,这个类就被回收了
匿名内部类
顾名思义,没的名字的类
public static void main(String[] args) {
new Outer(){
//内部类中的方法或属性
};
}
new关键字之后的可以用接口名或者类名都可,内部类的简化写法
静态内部类
static 是不能用来修饰类的, 但是成员内部类 可以看做是 外部类中的 一个成员,
那这样我是不是可以用 static 修饰呢?
这种用 static 修饰的 内部类, 我们称作 静态内部类, 也称作 嵌套内部类
那这么说的话…静态内部类就不能使用 外部类的 非静态属性 和 非静态方法
public class Outer {
public static int anInt = 100;
static class InnerStaticClass{
public static void print(){
System.out.println(anInt);
}
}
}
详细可以了解了解他人写的这篇博客Java内部类详解
==== 希望大佬评论指出问题 ❤️
==== 如果对你有帮助,小菜鸟需要鼓励,点个赞吧O(∩_∩)O😛~
全文参考内容:
《The Java® Virtual Machine Specification Java SE 8 Edition》
《深入理解Java虚拟机第三版》
《深入剖析Java虚拟机》
菜鸟教程JAVA