类和对象

类是一个抽象的概念,本质上是现实世界中某些事物具有的共同特征,将这些共同特征提取出来形成的概念就是一个类。

对象:由类创建的个体,也叫实例。

实例化:通过类这个模板创建对象的过程叫做 实例化。

共同特性:

  1. 状态特征 -> 属性
  2. 动作特征 -> 方法

类 = 属性 + 方法

创建一个类:

public class Student {
    String name;
    int age;
    int score;

    public Student(String name, int age, int score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }
}

在语法级别上创建一个对象:

  Student tom = new Student("Tom", 18, 100);

new运算符做了两件事情:

  1. 根据方法区中对象的大小在堆内存中申请空间(同时对所有成员变量默认初始化)
  2. 调用构造方法

属性在java中以 成员变量 的形式存在,这种成员变量又称为实例变量,实例变量实际上就是对象级别的变量

所以说 访问实例变量就必须要先创建对象

内存图如下所示:

tom保存的是堆内存中的地址,这个变量被称为引用,存储在栈内存中;创建出的对象本身只会存在堆上

对象的内存结构

对象由四部分组成:

  • MarkWord
  • ClassWord
  • 成员变量
  • 对象对齐
  1. MarkWord
  • 占用 8 个字节
  • 作用
    • 这个无法观察到,在 GC 发生时临时存储地址,ZGC 是在堆外记录地址,没有用 MarkWord
    • GC 时,移动对象,需要用它记录对象新的地址
      GC 时,记录对象年龄
    • 当没有重写 hashCode 方法时,存储 identity hash code 值
    • 存储锁信息
  1. ClassWord

ClassWord指向的是方法区的Class信息, 对象可以通过这个类指针访问自己的类信息和方法信息。
指向的是运行时动态数据结构。

内存占用:

  • 启用类指针压缩会占用4个字节(默认)
  • 不启用类指针压缩会占用8个字节(-XX:-UseCompressedClassPointers
  1. 成员变量

类型占用的字节数

  • 引用类型

    • 启用压缩会占用 4 字节(默认)
    • 不启用压缩会占用 8 字节(-XX:-UseCompressedOops
  • boolean,byte:1 字节

  • short,char:2 字节

  • int,float:4 字节

  • long,double:8 字节

问题 : boolean 类型为何占用 1 字节(byte)而不是 1 位(bit)?

回答:

  • 避免出现 word tearing 问题,现在的硬件不能原子访问一个 bit,读写的最小单位是 byte

  • 例如

    假设 8 个 bit,后两位分别代表两个 boolean 变量 a 和 b,取值均为 false(0)
    0 0 0 0 0 0 0 0
    线程1 读取这 8 个 bit,修改 a(倒数第二位)为 true(1)
    0 0 0 0 0 0 1 0
    线程2 读取这 8 个 bit,修改 b(倒数第一位)为 true(1)
    0 0 0 0 0 0 0 1
    等线程2 写回时,就会覆盖掉线程1 的结果

  1. 对象补齐

对象字节数如果不是8的倍数,需要补齐
以上文中的Student类为例,内存结构:

对象的创建过程

以如下代码为例,说明对象的创建过程:

public class A{
 	int a = 0;   
    public A(){
        this.a = 10;
    }
}

类加载阶段

  1. 执行[[015-强化#类加载时机|类加载]] 加载 链接 初始化,如果该类有父类,先加载父类
  2. 类加载完成后,确定每个类成员变量(Field)的偏移量、是否需要补齐

加载完成后,在方法区中生成字节码信息:类A对象将来的内存结构

OFF SZ TYPE DESCRIPTION     VALUE
0   8  (object header:mark)  N/A
8   4  (object header:class) N/A
12  4  int A.a               N/A
Instance size:16bytes

到此为止,对象有多大就知道了(int A.a的偏移量为12)

分配空间

当执行到new A() 时,对应的字节码为:

8:new        #13       //class test1/A
11:dup 
12:invokespecial #21   //Method test1/A."init":()V

其中 行号8的new时根据方法区的对象信息,在堆中申请空间(16字节)

  • 0 – 7 存储MarkWord
  • 8 – 11 存储类指针
  • 12 – 15 存储 A.a

行号11的dup是将对象地址(this指针)复制一份,配合下面的构造方法调用

行号12的invokespecial用来执行<init>:()V构造方法,用在this指向的对象上调用

new关键字的作用:

  1. 分配内存空间

    根据方法区的字节码信息在堆内存上分配空间并进行实例变量默认初始化操作

  2. 调用构造方法

构造方法是一种特殊的方法,构造方法默认支持[[004-方法#构造方法重载|方法重载]],构造方法的执行阶段:

1. 调用父类构造方法
2. 进行显式初始化、构造方法初始化操作
3. 进行其他操作(print)

当一个类没有提供任何构造方法的时候,系统会提供一个默认的无参构造(缺省构造器),当一个类中提供了构造方法,系统就不再提供无参的构造方法(建议提供无参构造)。

在new申请内存空间时为实例变量指定的默认值:

数据类型 默认值
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0
boolean flase
char \u0000
引用类型 null

在构造方法中,执行完父类的构造方法会对成员变量依次进行 显式初始化、构造方法初始化

调用构造

A的构造方法中,Java字节码为:

0:aload_0
1:invokespecial#1  //Method java/lang/Object."<init>":()V
4:aload_0
5:bipush  10
7:putfield #7      //Field a:l

其中

  • 行号0、1是调用java.lang.Object的无参构造

  • 行号4、5、7共同完成给成员变量赋值的操作

    • 行号4的 aload_0 是加载this指针,用它可以定位到对象

    • 行号5的 bipush 用来准备常数10

    • 行号7的 putfield #7 用来找到给哪个成员变量赋值

    • #7就是常量池中的符号,根据它能找到是给A.a int 的Field赋值

    • putfield内部执行时,就可以根据this指针先找到堆中的对象

    • offset 8 位置找到类指针,根据类信息找到A.a的偏移量为12

    • offset 12 位置存入常数 10,按四个字节写入

完整的字节码:

 Compiled from "A.java"
public class LogDemo.A
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // LogDemo/A
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
// Methodref : {Class = java/lang/Object,NameAndType = <init>()V}
// Fieldref : {Class = LogDemo/A,NameAndType = a:I}
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // LogDemo/A.a:I
   #8 = Class              #10            // LogDemo/A
   #9 = NameAndType        #11:#12        // a:I
  #10 = Utf8               LogDemo/A
  #11 = Utf8               a
  #12 = Utf8               I
  #13 = Methodref          #8.#3          // LogDemo/A."<init>":()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               LLogDemo/A;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               SourceFile
  #24 = Utf8               A.java
{
  int a;
    descriptor: I
    flags: (0x0000)

  public LogDemo.A();
    descriptor: ()V
    flags: (0x0001) 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_0
         6: putfield      #7                  // Field a:I
         9: aload_0
        10: bipush        10
        12: putfield      #7                  // Field a:I
        15: return
      LineNumberTable:
        line 5: 0
        line 4: 4
        line 6: 9
        line 7: 15
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   LLogDemo/A;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: new           #8                  // class LogDemo/A
         3: dup
         4: invokespecial #13                 // Method "<init>":()V
         7: pop
         8: return
      LineNumberTable:
        line 10: 0
        line 11: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

示例

package StudentStaticDemo;

public class Student {
    private String name;
    private int age;
    private String gender;


    public static String teacher = "lisi";
    public void study(Student this){
        System.out.println(teacher);
    }
    public Student(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
    
    public static void main(String[] args) {
        Student s1 = new Student("zhangsan",22,"male");
        s1.study();
    }
}
  1. 类加载:加载main方法所在类的字节码文件,系统类加载器将加载请求委托给平台加载器,平台加载器将请求委托给启动类加载器,启动类加载器和平台类加载器无法完成加载任务,最终系统类加载器将Student.class文件加载入内存,(并且在加载Student.class文件之前会先加载父类Object的字节码文件,最终Student的所有父类以及引用到的其他类都会被加载进内存),在加载时通过类的全限定名获取二进制字节流,将静态存储结构转化为运行时数据结构

在方法区创建的Class<Student>对象:

  1. 链接:

    1. 验证,确保字节流中的信息符合虚拟机规范
    2. 准备:为类变量在静态区中分配内存,设定默认初始化值
    3. 解析:
      1. 如果在类加载时遇到引用了其他类,JVM是无法判断其他类是否被加载入内存的,此时JVM使用符号引用代表该类;在解析阶段会将符号引用替换为直接引用
      2. JVM针对类或接口、字段、方法等内容进行解析,方法信息会形成虚方法表 vtable
  2. 初始化:静态变量赋值以及初始化其他资源

  3. new开辟内存空间,所有成员变量默认初始化

  4. 构造方法执行

    1. 调用父类Object的无参构造,无参构造中对Object特征(继承来的变量)中的变量进行显式、构造初始化并执行其他操作

    2. 执行本类的显式、构造初始化

    3. 执行其他操作,本例无

实例变量显式赋值的操作只有构造方法执行的时候才会进行,类加载的时候并不会初始化实例变量的空间,那是因为实例变量是对象级别的变量。

带有继承结构的对象创建

public class A{
 	int a = 0;   
    public A(){
        this.a = 10;
    }
}
class B extends A{
    long b;
    public B(){
        this.b = 20;
    }
}

类加载

类A对象:

OFF SZ TYPE DESCRIPTION     VALUE
0   8  (object header:mark)  N/A
8   4  (object header:class) N/A
12  4  int A.a               N/A
Instance size:16 bytes

类B对象:

OFF SZ TYPE DESCRIPTION     VALUE
0   8  (object header:mark)  N/A
8   4  (object header:class) N/A
12  4  int A.a               N/A
16  8  long B.b
Instance size:24 bytes

这里有一个细节,A类和B类中成员变量a 的偏移量都是12,可以保证下面代码的顺利执行:

public void test(A x){
    x.a = 10;
}

此处参数可能是类A,也可能是类B;都可以根据相同的偏移量找到此成员变量

注意:此处B中的a是继承自A.a

分配空间

当执行new B()时,可以根据类B对象的内存空间,申请24字节的空间

  • 0-7 存储MarkWord
  • 8 – 11 存储类指针
  • 12 – 15 存储 A.a
  • 16 – 23 存储 B.b

调用构造

  • 执行父类构造时,会执行 this.a = 10
  • 执行子类构造时,会执行 this.b = 20

this.a寻址、赋值过程:

  1. 通过this指针找到刚才的对象B
  2. 根据B对象中的ClassWord找到类B,进而找到类B的父类:类A
  3. 确定A.a OFFSET = 12
  4. this偏移12 存入常数10,写入四个字节

this.b寻址、赋值过程:

  1. 先通过this指针找到对象B
  2. 再根据B对象中的ClassWord找到类B
  3. 确定B.b成员变量的偏移量是16
  4. this偏移16存入常数20,写入八个字节

super

  • 在属性隐藏使访问父类成员变量a
super.a = 30;
((A)this) = 30;

字节码文件相同,都是:

aload_0
bipush   30
putfield  #7   //Field test1/A.a:I

就是找A.a的偏移量而已,结果是12

  • 访问子类中的a
this.a = 40;
((B)this).a = 40;

字节码文件相同:

aload_0
bipush   40
putfield  #11  //Field a:I

就是查找B.a的偏移量而已

结论

  • 对象.成员变量

根据类型定位成员变量的偏移量,最后读取或写入

  • 对象.成员方法(参数)

根据对象找到类型,根据类型查找vtable表,执行方法调用(非static、final、构造方法);

本质上是: 对象.成员方法(类型 this,参数)

对象本无方法。

面试题

public class Demo {
    public static void main(String[] args) {
        B b = new B();
    }
}
class A{
    int a = 10;

    public A() {
        print();
    }

    public void print() {
        System.out.println(a);
    }
}
class B extends A{
    int a = 20;
    public B() {
        print();
    }

    public void print() {
        System.out.println(a);
    }
}

对该代码进行分析:

但是得到的结果是 0 20,并没有出现预想的20 20

在print方法中,输出时隐式调用了this(也就是B类的a),而第一个0在super期间输出,第二个20在super结束后输出;这也就说明了在super执行时,B类的显式初始化还没有完成,a的值是默认初始化的0;但是在B类构造方法中调用print()执行之前,a类的显式初始化完成,在super()和print()之间一定有隐藏的操作来完成显式初始化赋值。

使用javap -c -v .\A.class反编译A的字节码文件

 Last modified 2023年4月24日; size 506 bytes
  SHA-256 checksum 4d794e8649fca8fa19ce52c9d5ca941a0caabd2dc0f1bd00e480388d150f7e33
  Compiled from "Demo.java"
class BlockSearch.A
  minor version: 0
  major version: 61
  flags: (0x0020) ACC_SUPER
  this_class: #8                          // BlockSearch/A
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
{
  	int a;
    descriptor: I
    flags: (0x0000)

  public BlockSearch.A();
    descriptor: ()V
    flags: (0x0001) 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: bipush        10
         7: putfield      #7                  // Field a:I
        10: aload_0
        11: invokevirtual #13                 // Method print:()V
        14: return
      LineNumberTable:
        line 11: 0
        line 9: 4
        line 12: 10
        line 13: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LBlockSearch/A;

SourceFile: "Demo.java"

反编译字节码文件得出,在super后才进行显式初始化操作,也就是说在super当中访问到的a是在new分配内存空间时默认初始化赋值的a

这个问题的本质是:子类对实例变量的赋值初始化是在super执行之前完成还是执行之后完成。

从另一个角度理解,假设父类当中定义一个public变量name,子类与父类之间没有属性隐藏,父类对name显式初始化赋值zhangsan,子类构造方法对name进行构造方法初始化;如果子类构造方法初始化在父类显式初始化之前执行,父类给定的zhangsan一定会覆盖构造方法初始化的name值,也就说明了子类的构造方法初始化或显式初始化一定在super调用父类构造方法之后执行

对该结论进行测试:

public class Test {
    public static void main(String[] args) {
        new Son("lisi");
    }
}
class Father{
    String name = "zhangsan";

    public Father() {
        super();
        
        print();
    }

    public void print() {
        System.out.println("Father name " + name);
    }
}
class Son extends Father{
    public Son(String name) {
        super();
        this.name = name;
        print();
    }
    public void print() {
        System.out.println("Son name " + name);
    }
}
/*
Son name zhangsan
Son name lisi
*/

总结:

new关键字一共做了两件事:

  1. 分配内存空间
    1. 对象头
    2. 自己成员变量 + 继承的成员变量
    3. 对齐(8的整数倍)
  2. 调用构造方法

构造方法一共做了三件事:

  1. 调用父类构造方法
  2. 进行显式初始化、构造方法初始化操作
  3. 进行其他操作(print)

执行过程:

  1. new 分配内存空间,对所有成员变量默认初始化
  2. 执行B构造方法:super 调用父类A构造方法,A类构造方法进行
    1. super
    2. 对本类成员变量显式、构造初始化
    3. 执行其他操作:调用子类B的print方法,打印子类成员变量b
  3. 执行B构造方法:对本类成员变量进行显式初始化、构造初始化
  4. 执行B构造方法:执行其他操作

面试题引申

public class FuTest {  
	public static void main(String[] args) {  
		// 猜猜打印的内容  
		Zi zi = new Zi();  
	}  
	static class Fu {  
		int a = 10;  
		private void printA() {  
			System.out.println("Fu PrintA:" + this.a);  
		}  
		public Fu() {  
			this.printA();  
		}  
	}  
	  
	static class Zi extends Fu {  
		int a = 20;  
		public void printA() {  
			System.out.println("Zi PrintA:" + this.a);  
		}  
		public Zi() {  
			this.printA();  
		}  
	}  
}

将父类的printA方法改为private,输出的结果是什么?

Fu PrintA:10
Zi PrintA:20

父类构造方法this.printA调用的是本类中私有的构造方法,父类构造方法中隐含的参数:

public Fu(Fu this = new Zi()) {  
	this.printA();  
}  

可以调用成功,虽然子类无法继承private的方法,但是方法调用的流程是:

根据方法头绑定方法体执行,在编译阶段不需要绑定,编译器认为父类的构造方法调用的就是父类自己的printA方法,但是实际执行时,编译器会判断this是否指向了子类对象,如果是子类对象,再判断子类对象有没有覆盖这个方法。但是private修饰的方法不会参与这个过程,private修饰的方法在编译器就已经确定直接引用。

在[[第8章 虚拟机字节码执行引擎#动态分派]]中,有更深入的讲解

查看对象的内存结构

package test1;
public class A {
    int a;
    public A() {
        this.a = 0xaaaa;
    }
}

package test1;
public class B extends A{
    long b;
    public B() {
        this.b = 0xbbbb;
    }
}

import org.openjdk.jol.vm.VM;

public class Test1 {
    public static void main(String[] args) {
        B b = new B();
        System.out.println(Long.toHexString(VM.current().addressOf(b))); //返回Java对象的内存地址
        System.out.println();
    }
}

输出的711370ee0就是B对象在Java虚拟机中的内存地址

使用jhsdb hsdb打开图形界面

输入进程号,通过JPS查看进程号:

输入19272,打开Memory Viewer:

输入内存地址:

查看类型指针对应的类布局:

但是注意:直接输0x00c00c00会报错:

因为该地址是被压缩的,需要加一个偏移量:

static

以static关键字修饰的都是类级别的,都采用类名.的方式进行访问(静态变量、静态方法)

方法体内声明的是局部变量,方法体外的变量是成员变量,成员变量可以分为:实例变量/静态变量

静态、实例、构造方法运行的时候都会入栈; 但是静态变量在类加载的时候就进行初始化,也就是说静态变量的内存空间在不需要new对象就开辟出来了,静态变量储存在堆内存的静态区。

类加载阶段:为静态变量分配内存空间,默认初始化,准备阶段:对静态变量进行初始化

在JDK8之后,静态存储位置(静态区)挪到了堆内存当中:

空引用在访问静态方法/变量时不会产生空指针异常

c1 = null;
c1.doSome();

实际上c1还是看作类Chinese,静态变量不需要对象,编译器在检测到2行代码时直接把c1翻译为Chinese(也就是说明了:引用所指向的对象并没有参与)

静态方法是解析调用的,字节码是invokestatic,只与静态类型有关

静态相关的都不需要new对象,直接采用类名.访问(也可以采用引用.访问,但不建议)

实例相关的都需要new对象,必须采用引用.访问

工具类

可以做一些事情,但是不描述任何事物的类

特点:

  • 构造方法私有化
private 类名(){
    
}

工具类不是描述事物的,创建该类对象没有任何意义

  • 方法都是静态方法

定义数组工具类

编写工具类:ArrayUtil

  • 提供方法:printArr(int[] arr),用于返回字符串类型的数组内容
  • 提供方法:getAverage(double[] arr),用于返回数组平均值
  • 定义测试类,调用该工具类的工具方法,返回结果
public class ArrayUtil {
    private ArrayUtil() {
    }
    public static String printArr(int[] arr){
        StringJoiner joiner = new StringJoiner(", ","[","]");
        for (int i = 0; i < arr.length; i++) {
            joiner.add(String.valueOf(arr[i]));
        }
        return joiner.toString();
    }
    public static double getAverage(double[] arr){
        double sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }
        return sum / arr.length;
    }
}

代码块

静态代码块

语法:

static{
	code;
}

随着类加载而加载,只执行一次

一般用来进行数据初始化

静态代码块在类加载的时候执行(在main方法之前执行),并且只执行一次;这种语法机制提供了一个类加载时机; 例如要求在类加载的时候解析某个文件并且只解析一次,那么此时就可以把解析该文件的代码写在静态代码块当中了。

public class StaticTest01 {
    static int i = 100;
    static {
        System.out.println(i);  //此处可以访问i
    }
    
    int k = 10;
    static {
        System.out.println(k); /*此处报错原因:k是实例变量,在构造方法执行的时候内存空间才会开辟*/
    }
    
    static {
        System.out.println(name);/*静态变量定义在静态代码块下方会报错:非法前向引用
           因为静态代码块和静态变量都是在类加载的时候执行,只能靠代码先后顺序决定谁先谁后*/
    }
    static String name = "liHua";
    
    public static void main(String[] args) {
    }
}
  1. 方法体中的代码是有顺序要求的,自上而下执行
  2. 静态代码块1和静态代码块2是有先后顺序要求的
  3. 静态代码块和静态变量是有先后顺序要求的

引例:

public class test {
    static int value = 9;

    public static void main(String[] args) {
        new test().printValue();
    }
    public void printValue(){
        int value = 99;
        System.out.println(this.value);
    }
}

输出的结果是9

  1. 两个value处在不同的域当中:一个在类体域,一个在方法体域,编译不会报错(在同一个域中变量不能重名)
  2. 输出的是this.value 也即是当前对象(类)中的value

输出判断

public class Text {
    public static int k = 0;
    public static Text t1 = new Text("t1");
    public static Text t2 = new Text("t2");
    public static int i = print("i");
    public static int n = 99;
    public static int j = print("j");

    static {
        print("静态块");
    }

    public Text(String str) {
        System.out.println((++k) + ":" + str + " i=" + i + " n=" + n);
        ++i;
        ++n;
    }
    public static int print(String str){
        System.out.println((++k) + ":" +str + " i=" + i + " n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String[] args) {
        new Text("init");
    }
}
/*
1:t1 i=0 n=0
2:t2 i=1 n=1
3:i i=2 n=2
4:j i=3 n=99
5:静态块 i=4 n=100
6:init i=5 n=101*/

局部代码块

可以提前结束变量的生命周期

public class Test {
    public static void main(String[] args) {
        {
            int a = 10;
            System.out.println(a);
        }
    }
}

但是现在是用不到的,这个机制只是为了节省内存

实例代码块

实例代码块提供了 对象构造时机; 调用构造方法时实例代码块先执行

{ //实例语句块
    code1;
    code2;
    code3;
}

可能在一个类中有很多构造方法,而这些构造方法前几行都是相同的,就可以把这些相同的代码写在实例代码块中

public class ConstructorTest01 {
    public static void main(String[] args) {
        new ConstructorTest01();
        new ConstructorTest01("str");//但是这两个对象并没有引用指向,在结束之后就被GC回收了
    }
    {
        System.out.println("实例代码块执行");  //每次构造方法执行之前,实例代码块先执行
    }
    public ConstructorTest01(String str) {
        System.out.println("有参构造执行");
    }

    public ConstructorTest01() {
        System.out.println("无参构造执行");
    }
}
  • 重写实例代码块
    Student s1 = new Student(1, "小亮", 99);  
    Student s2 = new Student(2, "小勇", 85);  
    Student s3 = new Student(3, "小响", 90);  
    Student s4 = new Student(4, "小强", 89);  
  
    new ArrayList<Student>(){{  
        add(s1);  
        add(s2);  
        add(s3);  
        add(s4);  
    }}.stream().sorted(Comparator.comparingInt(Student::getScore)).forEach(System.out::println);  
}

封装

对象代表什么,就要封装对应的数据,并且提供数据对应的行为

需求:人画圆

public class Person{
    
}
class Circle{
    double radius;
    public void draw(){
        System.out.println("radius" + radius);
    }
}

封装在Circle类当中,实际上是因为圆是Circle类画的,只是人调用了Circle类的draw方法

需求:人关门

门是门自己关的,人只是给了门一个作用力

public class Door{
    boolean flag = true;// 门的状态
    public void open{
        //开门
    }
    public void close{
        //关门
    }
}

JavaBean

  • 用来描述一类事物的类,专业叫做:Javabean类;在Javabean类中是不写main方法的
  • 编写main方法的类叫做 测试类

IDEA插件:PTG

可以生成无参、全参构造方法、get、set、toString方法

数据和数据业务相分离

实体类的对象只负责数据存取,对数据的处理交给其他类的对象来完成。

全类名也叫全限定名,可以通过import简化:

  • 使用同一个包中的类,不需要导包
  • 使用java.lang包中的类,不需要导包
  • 其他情况都需要导包
  • 如果同时使用两个包中的同名类,需要使用类全名

继承

假如我们要定义如下类:
学生类,老师类和工人类,分析如下。

  1. 学生类
    属性:姓名,年龄
    行为:吃饭,睡觉

  2. 老师类
    属性:姓名,年龄,薪水
    行为:吃饭,睡觉,教书

  3. 班主任
    属性:姓名,年龄,薪水
    行为:吃饭,睡觉,管理

如果我们定义了这三个类去开发一个系统,那么这三个类中就存在大量重复的信息(属性:姓名,年龄。行为:吃饭,睡觉)。这样就导致了相同代码大量重复,代码显得很臃肿和冗余,那么如何解决呢?

假如多个类中存在相同属性和行为时,我们可以将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。如图所示:

其中,多个类可以称为子类,单独被继承的那一个类称为父类、超类(superclass)或者基类。

继承是子类继承父类的特征和行为,使得子类对象具有父类的属性,或子类从父类继承方法,使得子类和父类有相同的行为。继承符合的关系是 IS-A(Cat IS-A Animal),父类更通用,子类更具体(更强大),子类会具有父类的一般特性也会有自身的特性

继承概念下的IS-A是一个单向的关系;如果X IS-A Y是成立的话,隐喻着X可以完成Y所有的行为(并且可能做出更多的行为)

继承机制最基本的作用是解决了代码复用,还有两个很重要的作用是:有了继承才会衍生出方法的覆盖和多态机制。

继承相关特性

  • B类继承A类,A类称为父类、超类、基类,B类称为子类、派生类、扩展类
  • Java中的继承是单继承,不支持多继承,C++支持多继承
  • 虽然Java中不支持多继承,但是有时会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说C直接继承B类,其实C间接继承A类
  • 如果一个类没有继承任何类,默认继承Object类
  • 继承会使得代码耦合度变高(组合是首选)
class X{} //继承Object
class Y extends X{} //Y继承X,Y间接继承Object
class Z extends Y{} //Z直接继承Y,Z间接继承X,Z间接继承Object

继承的意义:

  • 避免了重复的程序代码
  • 提出了共同的协议

继承:子类可以直接访问父类中的非私有的成员(成员变量、成员方法)。

继承强调的是一种关系,通过继承关系,在子类中可以访问父类的实例变量(非私有的)、实例方法。对于对象来说,因为继承关系的存在JVM在创建子类对象时会将父类的成员变量和子类的成员变量一同初始化并保存在子类对象中。

继承中对象和类是两个概念,对象是由类实例化出来的,自然会包含子类和父类的所有实例变量,而类间的继承只是产生了某种关联,在子类中可以访问父类中定义的成员变量、成员方法,权限修饰符和继承原本没有关系,只是因为private限制了修饰的成员只能在本类中访问,所以才说private修饰的成员无法被继承。

在对象实例化之后,super调用父类构造方法,父类中的private成员也会被实例化,保存至子类对象的父类型特征中,所以子类对象的空间会包含所有的成员变量。

在Java中,当子类调用父类的字段时,JVM使用继承关系和字段访问规则来确定要访问的字段。以下是JVM如何根据继承关系找到对应的字段的一般步骤:

  1. 类加载:当类加载器加载子类和父类的类文件时,它会构建类的内部数据结构,包括字段信息。这些字段信息包括字段的名称、类型和访问修饰符。

  2. 继承关系:JVM维护一个继承层次结构,其中子类包含对其父类的引用。这个关系是通过类的字节码文件中的特殊标记来建立的。

  3. 字段查找:当子类需要访问一个字段时,JVM首先查看子类自身的字段列表。如果在子类中找到匹配的字段,JVM将使用该字段。

  4. 向上搜索:如果子类没有匹配的字段,JVM会向上搜索继承层次结构,查看父类中是否有匹配的字段。这个搜索过程会递归进行,直到找到匹配的字段或达到继承层次的根(通常是java.lang.Object)。

  5. 访问权限检查:一旦找到匹配的字段,JVM还会检查字段的访问修饰符,确保子类有权限访问该字段。私有字段和一些其他受保护的字段可能无法被子类直接访问。

总之,JVM会根据继承关系和字段查找规则来确定要访问的字段。这使得子类能够继承和访问父类的字段,同时也可以在子类中定义自己的字段。

JVM虚拟机规范规定的字段访问步骤:

Java虚拟机规范(Java Virtual Machine Specification)详细描述了Java虚拟机的工作原理,包括继承和字段查找的规则。具体来说,它定义了以下几个步骤来查找字段:

  1. 查找字段符号引用:当在Java字节码中遇到对字段的引用时,首先需要解析字段符号引用,它包括字段的类名、字段名称和字段描述符。

  2. 类加载:虚拟机首先加载该字段所属的类。如果该类还没有加载,虚拟机会使用类加载器加载它。加载过程会包括类初始化,但不会导致该类的实例化。

  3. 查找字段:虚拟机在类的字段表中查找匹配字段名称和描述符的字段。这个搜索从子类开始,然后向上遍历继承层次结构,一直到Object类。

  4. 访问权限检查:一旦找到了匹配的字段,虚拟机会检查字段的访问修饰符。如果字段是private的或者访问权限不足,虚拟机将拒绝访问。

这些步骤确保了Java虚拟机在继承关系中正确查找字段,并根据访问权限进行访问控制。这些规则在Java语言中的继承机制中起到关键作用,确保了代码的正确性和安全性。这些规则可以在Java虚拟机规范的章节中找到,特别是在”Field Resolution”和”Field Access”等章节中有详细描述。

JVM虚拟机规范规定的方法访问步骤:

对于invokestatic、invokespecial来说,在编译阶段就确定了唯一的方法引用,对应了静态方法、私有方法、final方法、父类方法、构造方法

当子类调用父类的方法时,JVM使用继承关系和方法查找规则来确定要调用的方法。以下是JVM如何根据继承关系找到对应的方法的一般步骤:

  1. 类加载:当类加载器加载子类和父类的类文件时,它会构建类的内部数据结构,包括方法信息。这些方法信息包括方法的名称、参数列表和访问修饰符。

  2. 继承关系:JVM维护一个继承层次结构,其中子类包含对其父类的引用。这个关系是通过类的字节码文件中的特殊标记来建立的。

  3. 方法查找:当子类需要调用一个方法时,JVM首先查看子类自身的方法列表。如果在子类中找到匹配的方法,JVM将使用该方法。

  4. 向上搜索:如果子类没有匹配的方法,JVM会向上搜索继承层次结构,查看父类中是否有匹配的方法。这个搜索过程会递归进行,直到找到匹配的方法或达到继承层次的根(通常是java.lang.Object)。

  5. 方法覆盖:如果找到匹配的方法,JVM还会检查方法是否被子类覆盖(Override)。如果子类覆盖了父类的方法,JVM将调用子类中的覆盖方法而不是父类的原始方法。

总之,JVM会根据继承关系和方法查找规则来确定要调用的方法。这使得子类能够继承和调用父类的方法,同时也可以在子类中覆盖这些方法,实现方法的多态性。

可以继承的内容

构造方法 实例变量 实例方法
不能 可以(private也可以) 可以(private除外)

Oracle官方文档说明:

A subclass does not inherit the private members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.
子类不能继承父类的私有属性,但是如果父类中公有的方法影响到了父类私有属性,那么这些方法是能够被子类使用的

但是,父类的任何成员变量都是会被子类继承下去的。实际上,private、public、protected和继承没有关系,他们对成员方法和变量的限制只是在成员的可见性上

public class BaseClass{
    // 私有动态数组成员,注意它是"private"的
    private Vector objects;
    
    public BaseClass(){
        objects=new Vector();
    }
    
    /**
     * 公有函数,向动态数组成员objects添加字符串
     * @param str
     */
    @SuppressWarnings("unchecked")
    public void addStr2Obs(String str){
        objects.add(str);
    }
    
    /**
     * 公有函数,打印objects中的元素
     */
    public void printAll(){
        for(int i=0;i<objects.size();i++){
            System.out.println("序号="+i+"\t元素="+objects.get(i));
        }
    }
}

ChildClass,BaseClass的派生类:

/**
 \* ChildClass,BaseClass的派生类
 */
public class ChildClass extends BaseClass{
  public void printObjects(){
    // 下面是不能编译通过的
    /*for(int i=0;i<objects.size();i++){
      System.out.println("序号="+i+"\t元素="+objects.get(i));
    }*/
  }

  public static void main(String[] args){
    ChildClass childClass=new ChildClass();
    
    childClass.addStr2Obs("Hello");
    childClass.addStr2Obs("World");
    childClass.addStr2Obs("China");
    childClass.addStr2Obs("sitinspring");
    
    childClass.printAll();
  }
}

把断点停在main函数中的childClass.printAll()上:

证明:objects确实是ChildClass类实例childClass的成员,而且四个字符串也都被加进去了。

某些书中关于private限制的成员变量是这样写的:private 只允许来自该类内部的方法访问,不允许任何来自该类外部的访问。

上面添字符串和遍历输出方法都是BaseClass的成员,所以它当然被这两个方法访问;而ChildClass的printObjects是BaseClass类外部的方法,结果当然是编译也不能通过。

实际上,private、public、protected和继承没有关系;他们对成员方法和变量的限制只是在成员的可见性上。

  • public允许来自任何类的访问;
  • private只允许来自该类内部的方法访问,不允许任何来自该类外部的访问;
  • protected允许来自同一包中的任何类以及该类的任何地方的任何子类的方法访问;

而关于成员变量的继承,父类的任何成员变量都是会被子类继承下去的,私有的objects就是证明。这些继承下来的私有成员虽对子类来说不可见,但子类仍然可以用父类的方法操作他们.

这样的设计有何意义?

我们可以用这个方法将我们的成员保护得更好,让子类的设计者也只能通过父类指定的方法修改父类的私有成员,这样将能把类保护得更好。这对一个完整的继承体系是尤为可贵的,JDK源码就有这样的例子:java.util.Observable就是这样设计的。

实例变量继承的内存图

  • 先加载父类字节码,再加载子类字节码
public class Fu {
    String name;
    int age;
}
class Zi extends Fu{
    String game;
}
class Test{
    public static void main(String[] args) {
        Zi z = new Zi();
        System.out.println(z);
        z.name = "zhangsan";
        z.age = 23;
        z.game = "Feria";
        System.out.println(z.name + " ," + z.age + " ," + z.game);
    }
}

实例方法继承的内存图

虚方法表在类加载的链接-解析阶段创建

在继承链中,最顶级的类会创建一个虚方法表,该表中包含了可能会经常使用到的方法,这些方法有以下要求:

  • 非private
  • 非static
  • 非final

然后将虚方法表交给下级类,下级类在此方法表的基础上再添加自己类中的虚方法,一直类推

查找A类的虚方法表,如果有c方法直接加载该方法执行。

如果要通过A类实例调用的C类中方法不是虚方法,还是会逐层向上查找。

public class Fu {
    public void fuShow1() {
        System.out.println("public --- FuShow1");
    }

    private void fuShow2() {
        System.out.println("private --- FuShow2");
    }
}

class Zi extends Fu {
    public void ziShow(){
        System.out.println("public --- ZiShow");
    }
}

class Test {
    public static void main(String[] args) {
        Zi z = new Zi();
        System.out.println(z);
        z.ziShow();
        z.fuShow1();
        z.fuShow2();/*'fuShow2()' has private access in 'Fu'*/
    }
}

此时字节码文件才加载完毕

调用时先判断是否是虚方法,如果是虚方法就会从虚方法表中直接调用;

是虚方法,从子类的虚方法表中直接拿到该方法加载进内存

final、static的方法虽然不能被添加到虚方法表中,但是还是可以继承的;只是在访问的时候:

  • static:引用被翻译为类名,还是通过类名访问(不存在空指针异常)
  • final:不能被添加到虚方法表中,如果在子类中访问父类中定义的final方法,需要一层一层向上查找

private的实例方法的继承问题

子类是可以继承父类的所有属性和方法的,包括private。

但是这里的继承实际上是 “拥有”,并非是 “可调用的”;private、public修饰符针对的是可调用的作用域范围。

package demo07.test;

public class Father
{
    private void method()
    {
        System.out.println("父类private的方法");
    }

    public static void main(String[] args)
    {
        Father father = new Son();
        father.method(); //父类private的方法
    }
}


package demo07.test;
public class Son extends Father
{

}

运行是可以通过的,并且输出:父类private的方法

这是[[005-面向对象核心#面试题引申|面试题引申]]的简化版本,这样做就是在父类中特有,而子类中没有的方法。

静态绑定:编译器认为father的Father类型中包含了method方法,编译通过;而private不参与动态绑定,在编译期就确定了直接引用。

字节码文件:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #21                 // class demo07/test/Son
         3: dup
         4: invokespecial #23                 // Method demo07/test/Son."<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #24                 // Method method:()V
        12: return
      LineNumberTable:
        line 16: 0
        line 17: 8
        line 19: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      13     0  args   [Ljava/lang/String;
            8       5     1     f   Ldemo07/test/Father;
  
#24 = Methodref          #25.#26        // demo07/test/Father.method:()V

分析

多态是不能调用子类中特有的方法的,如果要调用,就必须向下转型。如果是父类中特有,而子类中没有的方法呢?(根据Java官方文档我们知道这是不能做到的),在此之前的想法是,只要把父类中的方法用private修饰,子类就不会继承了。猜测:编译可以通过,运行会报错。

测试:

package demo07.test;
//父类,有一个private修饰的方法
public class Father{
    private void method(){
        System.out.println("父类private的方法");
    }
}

package demo07.test;
//子类,什么都没写
public class Son extends Father{

}

测试类:

package demo07.test;
//测试类
public class Test01
{
    public static void main(String[] args)
    {
        Father father = new Son();
        father.method();//报错:'method()' has private access in 'demo07.test.Father'
    }
}

但是,private的方法在其他类(测试类)中无法调用,在测试类中是访问不到的,编译会报错

只对本类可见,就将main方法放在父类中测试:

package demo07.test;

public class Father
{
    private void method()
    {
        System.out.println("父类private的方法");
    }

    public static void main(String[] args)
    {
        Father father = new Son();
        father.method();
    }
}

这样就不会报错了,但是程序可以运行,并且输出:父类private的方法

子类中明明没有这个方法,为什么可以运行成功?(问题1

原因可能在于访问权限修饰符,父类这个private修饰的method方法,其实是被子类继承下去了的,只是对子类不可见。但是还是有个问题:在父类的main方法中调用了父类的method方法的,private可以对本类可见,所以没有问题。但是:运行的时候,是在父类中运行 “子类的” private修饰的方法,那也应该不能成功运行才对,但实际上却成功运行了,这又是为什么?(问题2

问题2的意思是:在父类中,为什么可以调用子类的private方法?private限定了只能在本类中访问,那么在父类中理应不能进行调用;实际上这个问题可以变为:父类中一个方法被子类继承下去,那么这个方法的权限修饰符是针对哪一个类而言的?

访问权限修饰符表:

访问控制修饰符 本类 同包中其他类 不同包的子类 不同包的无关类
public 可以 可以 可以 可以
protected 可以 可以 可以 不行
可以 可以 不行 不行
private 可以 不行 不行 不行

如果父类中有一个protected修饰的方法被子类继承下去了,那么这些本类、同包、子类是针对父类而言还是针对子类而言?

假如父类子类不在同一个包当中,被继承的方法是protected修饰的,如果是针对父类而言的,在子类所在包下建立测试类,创建子类对象,应该是不能通过子类对象调用这个方法的,因为这个方法和父类不在同一个包当中。而在父类所在的包下建立测试类,创建子类对象(父类和子类是一样的),就可以访问这个方法

测试(注意所在包):

package test;
//test包下的父类
public class Father{
    protected void method(){
        System.out.println("父类protected修饰的方法");
    }
}
package test.test;
import test.Father;
//test.test包下的子类,什么都没写,但是继承了test包下的父类
public class Son extends Father{

}
  • 在子类的包下建立测试类:
package test.test;
//test.test包下的测试类(和子类同包)
public class Test01{
    public static void main(String[] args){
        Son son = new Son();
        son.method();//报错:'method()' has protected access in 'test.Father'
    }
}

和猜测相同,不能访问

  • 在父类的包下建立测试类:
package test;
import test.test.Son;
//test包下的测试类(和父类同包)
public class Test02{
    public static void main(String[] args){
        Son son = new Son();
        son.method();
    }
}

可以访问

结论:在继承关系中,子类从父类继承的方法,它的访问权限修饰符是针对父类而言的

如果子类重写了这个方法,访问权限修饰符就是针对子类而言的

访问权限修饰符

  • private表示私有的 只能在本类中访问
  • public表示公开的 任何位置都能访问
  • default表示只能在本类、同package下访问
  • protected表示只能在本类,同package,子类中访问
访问控制修饰符 本类 同包中其他类 不同包的子类 不同包的无关类
public 可以 可以 可以 可以
protected 可以 可以 可以 不行
可以 可以 不行 不行
private 可以 不行 不行 不行

如果一个方法是多个方法共性内容的抽取,这个方法一般设置为私有的

protected修饰符:

在不同包下的子类当中,是可以访问父类的protected修饰的doSome()方法的,因为这个方法一定是子类对象调用的(隐含参数this),protected修饰的方法只能在本类、子类、同包中访问

在子类的main方法中,可以通过子类对象访问到子类的doSome();protected的含义是指子类可以访问,说的是子类直接访问父类的protected方法,而不是说子类中,可以调用父类的对象访问父类的protected方法;

虽然是在子类中,但是却是使用父类的对象,调用父类的protected方法。这是在不同包中访问该protected方法,这样是不可行的

在子类中新建子类对象访问doSome()方法,JDK应该有某种机制可以检测到这是在本类中访问;但是在子类中新建父类对象访问这个方法,就无法被检测到。

f.doSome() 失败,因为在编译阶段,对doSome方法进行静态绑定,查找静态类型Father的虚方法表(访问的是虚方法),可以找到doSome方法,但是访问权限验证不通过,对于Father的doSome方法来说,此时是在类外、包外访问

s.doSome() 成功,编译阶段,对doSome方法进行静态绑定,查找静态类型Son的虚方法表(访问的是虚方法),可以找到doSome方法,权限验证成功,此时的访问是本类中进行访问

如果子类与父类同包,这样的访问就是没有问题的:

这样对于父类来说就是在同包下访问

在子类中访问父类的protected方法

如果在子类中直接访问父类的protected方法是没有问题的,如果在子类中创建父类的对象访问这个方法就不可以了(非同包下)

protected修饰的成员变量和方法可以被包外的子类访问到,被包外的子类访问并不是可以在子类中创建父类的对象进行访问,而是在子类中直接进行访问(也就是隐含的this进行访问)

在本类中可以访问,修饰的方法会交给子类的虚方法表,通过引用可以在同包中调用该方法

Object虚方法表

对继承自Object的方法进行测试

注意:当源码中一个方法以;结尾并且修饰符列表中有native关键字表示底层调用C++写的dll程序(dll动态链接库文件)

Object类中的toString方法:

 public String toString() {
        return this.getClass().getName() + "@" + Integer.toHexString(this.hashCode());
    }

测试:

public class ExtendsText01 {
    public static void main(String[] args) {
        ExtendsText01 ex = new ExtendsText01();
        System.out.println(ex);//ExtendsText01@2d98a335
    }
}
  • 2d98a335是随机数经过哈希算法得到的十六进制结果
  • 如果println输出一个引用的话,会自动调用该引用的toString方法

对于hashCode()方法:

openjdk中查看C++程序:

static inline intptr_t get_next_hash(Thread* current, oop obj) {
  intptr_t value = 0;
  if (hashCode == 0) {
    // This form uses global Park-Miller RNG.
    // On MP system we'll have lots of RW access to a global, so the
    // mechanism induces lots of coherency traffic.
    value = os::random(); //1. 随机数作为hashCode
  } else if (hashCode == 1) {
    // This variation has the property of being stable (idempotent)
    // between STW operations.  This can be useful in some of the 1-0
    // synchronization schemes.
    intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3; //读取内存地址
    value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random; //2. 内存地址结合随机数运算作为hashCode
  } else if (hashCode == 2) {
    value = 1;            // for sensitivity testing  3. 固定为1
  } else if (hashCode == 3) {
    value = ++GVars.hc_sequence;  // 4. 自增的序列作为hashCode
  } else if (hashCode == 4) {
    value = cast_from_oop<intptr_t>(obj); // 5. 对象的内存地址作为hashCode
  } else {
    // Marsaglia's xor-shift scheme with thread-specific state
    // This is probably the best overall implementation -- we'll
    // likely make this the default in future releases.
      
    //6. 随机数算法
    unsigned t = current->_hashStateX;
    t ^= (t << 11);
    current->_hashStateX = current->_hashStateY;
    current->_hashStateY = current->_hashStateZ;
    current->_hashStateZ = current->_hashStateW;
    unsigned v = current->_hashStateW;
    v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
    current->_hashStateW = v;
    value = v;
  }

  value &= markWord::hash_mask;
  if (value == 0) value = 0xBAD;
  assert(value != markWord::no_hash, "invariant");
  return value;
}

默认采用第6种 随机数算法

hashCode选项

  • hashCode == 0 随机数
  • hashCode == 1 地址^随机数
  • hashCode == 2 固定值1
  • hashCode == 3 递增数列
  • hashCode == 4 地址
  • hashCode == 5 随机数

使用参数

  1. -XX:+UnlockExperimentalVMOptions 解锁实验性质的虚拟机选项
  2. -XX:hashCode=? 指定hashCode生成方式

对象刚创建时hashCode是不存在的,第一次调用对象的hashCode方法时产生hashCode值,采用第六种策略计算出该值并存入对象的MarkWord部分,之后每次调用都从MarkWord部分读取

继承后的执行顺序

public class FuTest {

    public static void main(String[] args) {
        // 0:发现要new Zi,而此时内存中没有Zi这个类,而Zi又继承了Fu,所以会先加载 Fu、再加载 Zi(注意,此时只是类加载!)
        // 5:【类加载并初始化】完毕,开始【对象创建和初始化】
        Zi zi = new Zi();
    }

    static class Fu {
        // 类加载1:加载Fu,给Fu的静态字段默认初始化
        static int FU_STATIC_A = 10;

        static {
            // 类加载2:调用static代码块,给Fu静态字段初始化
            FU_STATIC_A = 11;
        }

        // 对象初始化8:初始化fu普通字段
        int a = 10;

        public void printA() {
            System.out.println("Fu PrintA:" + a);
        }

        public Fu() {
            // 对象初始化7:调用fu构造器
            printA();
        }
    }

    static class Zi extends Fu {
        // 类加载3:加载Zi,给Zi的静态字段默认初始化
        static int ZI_STATIC_A = 20;

        static {
            // 类加载4:调用static代码块,给Zi静态字段初始化
            ZI_STATIC_A = 21;
        }

        // 对象初始化9:初始化zi普通字段
        int a = 20;

        @Override
        public void printA() {
            System.out.println("Zi PrintA:" + a);
        }

        public Zi() {
            // 对象初始化6:优先初始化父对象
            super();
            // 对象初始化9:zi构造器执行完毕
            printA();
        }
    }
}

不希望类被继承的策略

  1. 存取控制:类不能被标记为私有的,但是类可以标记为不公开的,不公开的类只能被同一个包的类做出子类
  2. 使用final修饰符:表示这个类在继承树的末端
  3. 让类只有private的构造程序 ,提供静态方法传递该类对象

方法覆盖

从父类当中继承的方法不满足子类的使用需求时,就要考虑使用方法覆盖了;发生方法覆盖后子类对象会调用覆盖之后的方法

方法覆盖的条件:

  • 必须发生在具有继承关系的两个类之间;
  • 覆盖后的方法和原来的方法具有兼容的返回值类型,相同的方法名,相同的形式参数列表(子类的返回值类型必须是相同的类型或者是该类型的子类)
  • 只有虚方法表中的方法才可以被方法覆盖

注意:

  1. 私有方法无法被添加到虚方法表中,所以无法覆盖
  2. 构造方法无法被添加到虚方法表中,所以不能覆盖
  3. 覆盖之后的方法不能比原方法有更低的访问控制权限(可以从protected -> public)(不能在编译期间是公有的,然后在执行期间突然被JVM阻止)
  4. 覆盖之后的方法不能比原方法抛出更多/更宽泛的异常,可以相同或者更少,不能在编译期间显示异常被处理,运行期间显示未处理
  5. 方法覆盖只和方法有关,和属性无关
  6. 静态方法不存在覆盖【问题一】
  7. 只有被添加虚方法表中的方法才能被覆盖

例如:定义了一个动物类代码如下:

public class Animal  {
    public void run(){
        System.out.println("动物跑的很快!");
    }
    public void cry(){
        System.out.println("动物都可以叫~~~");
    }
}

然后定义一个猫类,猫类认为父类cry()方法不能满足自己的需求

代码如下:

public class Cat extends Animal {
    @Override
    public void cry(){
        System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
    }
}

public class Test {
	public static void main(String[] args) {
      	// 创建子类对象
      	Cat ddm = new Cat();
        // 调用父类继承而来的方法
        ddm.run();
      	// 调用子类重写的方法
      	ddm.cry();
	}
}

示例:

class Animal{
    void move(){
        System.out.println("Animal moving");
    }
    void sing(){
        
    }
}

要求子类Cat和Bird有不同的move方法:

class Cat extends Animal{
    @Override
    void move() {
        System.out.println("Cat wondering");
    }
}
class Bird extends Animal{
    @Override
    void move() {
        System.out.println("Bird flying");
    }
    void sing(int i){
        //此处sing和父类的sing没有发生方法覆盖,因为参数列表不同;但间接形成了方法重载
    }
}

@Override重写注解

  • @Override:注解,重写注解校验

  • 这个注解标记的方法,就说明这个方法必须是重写父类的方法,否则编译阶段报错。

  • 建议重写都加上这个注解,一方面可以提高代码的可读性,一方面可以防止重写出错!

    加上后的子类代码形式如下:

public class Cat extends Animal {
  @Override
  public void cry(){
	  System.out.println("我们一起学猫叫,喵喵喵!喵的非常好听!");
  }
}

方法覆盖的本质:覆盖虚方法表中的方法

如果B类型对象调用,就会查找B类的虚方法表

虚方法表中存放着各个方法的实际入口地址,如果某个方法在子类中没有被重写,那么子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向了父类的实现入口。如果子类重写了这个方法,子类虚方法表的地址也会被替换为指向子类实现版本的入口地址。

当要覆盖父类的方法时,就必须要“履行约定”,比如这个约定是:

boolean trunOn(){}

这就表示:没有参数且具有布尔类型的返回值

字节码分析方法覆盖

方法覆盖就是子类对父类的实例方法进行重新定义功能,且返回类型、方法名以及参数列表保持一致,且对重写方法的调用主要看实际类型。实际类型如果实现了该方法则直接调用该方法。如果没有实现,从子类型的虚方法表中选择继承自父类的方法进行实现。

问题:为什么只有对实例方法才能重写?

理解三个概念:静态类型、实际类型、方法接受者

Person student= new Student();
student.work();
  • 静态类型就是编译器编译期间认为对象所属的类型,这个主要这个主要根据声明类型决定,所以上述Person就是静态类型
  • 实际类型就是解释器在执行时根据引用实际指向的对象所决定的,所以Student就是实际类型。
  • 方法接受者就是动态绑定所找到执行此方法的对象,比如student。

class文件中方法的字节码调用指令:

(1)invokestatic:调用静态方法
(2)invokespecial:调用实例构造器方法,私有方法。
(3)invokevirtual:调用所有的虚方法。
(4)invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
(5)invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

public class Father {  
	public void doSome(){  
		System.out.println("Father's non-static method");  
	}  
	  
	public static void doOther(){  
		System.out.println("Father's static method");  
	}  
}  
  
class Son extends Father {  
	public void doSome(){  
		System.out.println("Son's non-static method");  
	}  
	  
	public static void doOther(){  
		System.out.println("Son's static method");  
	}  
  
public static void main(String[] args) {  
	Father f = new Father();  
	Son s = new Son();  
	Father f_s = new Son(); // 父类引用指向子类对象  
	  
	f.doSome(); //Father's non-static method  
	f.doOther(); //Father's static method  
	s.doSome(); //Son's non-static method  
	s.doOther(); //Son's static method  
	f_s.doSome(); //Son's non-static method  
	f_s.doOther(); //Father's static method  
	}  
}

最后一次调用时,没有动态绑定,之前的解释是引用被翻译为类名访问。

main方法的字节码:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class demo01/Father
         3: dup
         4: invokespecial #1                  // Method demo01/Father."<init>":()V
         7: astore_1
         8: new           #23                 // class demo01/Son
        11: dup
        12: invokespecial #25                 // Method "<init>":()V
        15: astore_2
        16: new           #23                 // class demo01/Son
        19: dup
        20: invokespecial #25                 // Method "<init>":()V
        23: astore_3
        
        24: aload_1
        25: invokevirtual #26                 // Method demo01/Father.doSome:()V
        28: aload_1
        29: pop
        30: invokestatic  #29                 // Method demo01/Father.doOther:()V
        33: aload_2
        34: invokevirtual #32                 // Method doSome:()V
        37: aload_2
        38: pop
        39: invokestatic  #33                 // Method doOther:()V
        42: aload_3
        43: invokevirtual #26                 // Method demo01/Father.doSome:()V
        46: aload_3
        47: pop
        48: invokestatic  #29                 // Method demo01/Father.doOther:()V
        51: return

f.doSome() 这句话是调用父类的实例方法,对应字节码中25行的invokevirtual 调用虚方法,并且此方法的引用存在于方法表中,使用invokevirtual 指令会去方法表中寻找要调用的方法的引用。

f.doOther() 这句话是调用父类的静态方法,对应字节码30行的invokestatic 调用静态方法,无需经过方法表,这也就解释了静态方法的执行只看静态类型,与实际类型无关;而方法重写关注的是实际类型,所以静态方法不能被重写,这也解释了方法覆盖的问题1

f_s.doSome() 这句话是父类型引用调用子类的实例方法,对应字节码43行的invokevirtual 调用虚方法,说明运行期间会到方法表中去调用真实指向的方法,因为doSome可能被重写,所以编译期间应标明在在运行时调用doSome所在方法表中存储的真正方法的引用,因为doSome被Son重写,所以Son方法表中原先存储Father的doSome方法被替换为Son的doSome方法引用,运行时根据invokevirtual 调用的虚方法是子类重写的

f_s.doOther() 对应字节码48行invokestatic 不能访问方法表,运行时就是父类的静态方法

编译时把对象的静态类型作为方法接收者,运行时期再根据指令集更改

invokevirtual

public class ClassReference {  
	static class Person {  
		@Override  
		public String toString(){  
			return "I'm a person.";  
		}  
		public void eat(){  
			System.out.println("Person eat");  
		}  
		public void speak(){  
			System.out.println("Person speak");  
		}  
	}  
	static class Boy extends Person{  
		@Override  
		public String toString(){  
			return "I'm a boy";  
		}  
		@Override  
		public void speak(){  
			System.out.println("Boy speak");  
		}  
		public void fight(){  
			System.out.println("Boy fight");  
		}  
	}  
	static class Girl extends Person{  
		@Override  
		public String toString(){  
			return "I'm a girl";  
		}  
		@Override  
		public void speak(){  
			System.out.println("Girl speak");  
		}  
		public void sing(){  
			System.out.println("Girl sing");  
		}  
	}  
	public static void main(String[] args) {  
		Person boy = new Boy();  
		Person girl = new Girl();  
		System.out.println(boy);  //I'm a boy
		boy.eat();                //Person eat
		boy.speak();              //Boy speak
		System.out.println(girl); //I'm a girl
		girl.eat();               //Person eat
		girl.speak();             //Girl speak
	}  
}

Boy和Girl没有重写父类的eat方法,会调用父类的eat方法

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #7                  // class demo02/ClassReference$Boy
         3: dup
         4: invokespecial #9                  // Method demo02/ClassReference$Boy."<init>":()V
         7: astore_1
         8: new           #10                 // class demo02/ClassReference$Girl
        11: dup
        12: invokespecial #12                 // Method demo02/ClassReference$Girl."<init>":()V
        15: astore_2
        16: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload_1
        20: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        23: aload_1
        24: invokevirtual #25                 // Method demo02/ClassReference$Person.eat:()V
        27: aload_1
        28: invokevirtual #30                 // Method demo02/ClassReference$Person.speak:()V
        31: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
        34: aload_2
        35: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
        38: aload_2
        39: invokevirtual #25                 // Method demo02/ClassReference$Person.eat:()V
        42: aload_2
        43: invokevirtual #30                 // Method demo02/ClassReference$Person.speak:()V
        46: return

在16行,会将根类Object的toString方法引用写入class文件,编译器会将祖先的方法引用写入而非近亲

方法表在内存中的模型:

继承的方法从头到尾排列,方法在子类中有固定的索引,有相同的偏移量;如果子类重写某个方法,就会使子类方法表原先存在父类的方法引用变为重写后的方法引用,这就是为什么通过对象类型可以调用到正确的方法

girl.speak() 为例,解释INVOKEVIRTUAL 指令的流程

  1. 首先 invokevirtual demo02/ClassReference$Person.speak:()V 中,根据 demo02/ClassReference$Person.speak:()V 在常量池中查找该方法的偏移量
  2. 查看Person的方法表,得到speak方法在该方法表中的偏移量,得到该方法的直接引用
  3. 根据this实例判断该引用指向girl对象
  4. 在Girl方法表中,根据上面的偏移量在方法表中找到该方法引用,因为该方法引用的值在类加载时根据是否重写了方法已经确定了正确的方法引用,所以可以直接调用该方法

总结:在编译器将类编译为class文件时,方法会根据静态类型从而将对应的方法引用写入class中;运行时JVM会根据INVOKEVIRTUAL 所指向的方法引用在常量池中找到该方法的偏移量,再根据this找到引用类型所指向的真实对象,访问这个对象类型的方法表,根据偏移量找出存放目标方法引用的位置,取出这个引用,调用这个引用实际指向的方法,完成多态。

this

  • this是一个变量,一个引用 保存当前对象的内存地址,指向自身,严格来说this指向的就是当前对象
  • this只能用于实例方法当中,谁调用这个方法this就代表谁(实例方法一定是对象来触发的)
  • this不能出现在静态方法当中,因为静态方法不需要对象的参与,也就是说调用的时候不需要先创建对象,直接通过类名.的方式访问,而this代指当前对象,这两者是冲突的,也就是说静态方法不知道这个是哪个对象的成员变量
  • this大多数情况下都是可以省略的

public class Student {
    String name;
    int age;
    public void show(){
        System.out.println(name + ", " + age);
    }
}
public class Test {
    public static void main(String[] args) {
        Student s = new Student();
        s.name = "zhangsan";
        s.age = 23;

        s.show();

        //把对象在内存中的结构打印出来
        ClassLayout layout = ClassLayout.parseInstance(s); 
        System.out.println(layout.toPrintable());
    }
}

如果show方法添加三个局部变量,再查看字节码信息:

    public void show(){
        int a = 10;
        int b = 20;
        int c = 30;
        System.out.println(name + ", " + age);
    }

可以发现this只是一个局部变量

this的本质:所在方法调用者的地址值

以上两个age是不同的空间,age指的是局部变量,this.age是堆内存中的实例变量

this()

this在构造方法中的使用:

表示通过当前的构造方法调用本类中另外一个构造方法,格式如下:

this(args);

这个语法机制实现了:代码复用;

注意:this()只能出现在构造方法的第一行,并且只能使用一次。(否则会报错:对this的调用必须时构造器中的第一个语句)

实例方法的隐含参数this

public class Student {
    private String name;
    private int age;
    private String gender;
    
    public static String teacher;
    
    public void study(){
        System.out.println(this.name + "study");
    }
}

静态方法中没有this关键字,对于实例方法来说,形参列表是有一个隐藏的this:

    public void study(Student this){
        System.out.println(this.name + "study");
    }

但是注意:这个参数是在调用方法的时候由虚拟机对其赋值,不能手动赋值;谁调用这个方法,this就代表它的地址值

在study()方法中调用其他实例方法:

通过this找到堆内存中的对象,通过对象的ClassWord指针找到类对象,访问类对象的vtable来确认调用的是哪个方法

而对于静态方法来说:

就会直接报错

实例相关的数据都是与对象相关的,调用的时候必须找到对象(this的指向);而静态的数据是与对象无关的,是在类加载时就存在的,所以是没有this关键字的。

虽然不推荐使用引用去调用静态相关的数据,但是对于实例方法中访问静态数据,还是会用this去调用的:

继承中的this

内存

  • 静态方法不能调用实例变量:

静态方法是不会去对象内部查找该变量的,只会在静态区中查找

  • 静态方法不能调用实例方法:

调用show方法(实例方法)时,必须要对隐含的参数this进行传递,而静态方法中没有通过引用调用,JVM无法完成赋值操作。

  • 实例方法可以访问静态相关的数据:

self与this

以下是一段Python代码:

# 括号里的object,表示Student类继承自object。在Java里默认继承Object。当然,Python里不写也可以
class Student(object):

    # 构造函数,变量前面下划线,是访问修饰符,表示私有
    def __init__(self, name, age):
        self.__name = name
        self.__age = age

    # get方法,可不写。你会发现Python里方法形参都有self,其实就相当于Java里的this,只不过Java通常是隐式的
    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

    def print_info(self):
        print("姓名:" + self.__name, "年龄:" + str(self.__age))


# 你可以将下面代码理解为Java的main方法,这里用来测试
if __name__ == '__main__':
    # 调用构造函数得到对象,Python不需要new关键字
    student = Student("eun", 18)
    # 实际调用方法时并不需要传self,会默认传递
    print(student.get_age())
    print(student.get_name())
    student.print_info()

对象的本质是“多个相关数据的统一载体”。比如一个人,有name、age、height等社会或生理体征,而这些数据是属于一个个体的,如果用数组去存,表现力有所欠缺,无法表达“它们属于同一个个体”的含义。

但在Java中对象是在堆空间中生成的,数据会在堆空间占据一定内存开销。而方法只有一份。

方法为什么被设计成只有一份?

因为多个个体,属性可能不同,比如我身高180,你身高150,我18岁,你30了。但我们都能跑、能跳、能吃饭,这些技能(method)都是共通的,没必要和属性数据一样单独在堆空间各存一份,所以被抽取出来存放。

此时,方法相当于一套指令模板,谁都可以传入数据交给它执行,然后得到执行后的结果返回。

但此时会存在一个问题:张三这个对象调用了eat()方法,应该把饭送到他嘴里,而不是送到李四嘴里。那么方法如何知道把饭送到哪里呢?

换句话说:共性的方法如何处理特定的数据?

Python的self、Java的this其实就是解决这个问题的。你可以理解为对象内部持有一个引用,当你调用某个方法时,必须传递这个对象引用,然后方法根据这个引用就知道当前这套指令是对哪个对象的数据进行操作了。

static与this

static修饰的属性或方法其实都是属于类的,是所有对象共享的。之所以一个变量或者方法要声明为static,是因为

  • static变量:大家共有的,大家都一样,不是特定的差异化数据
  • static方法:这个方法不处理差异化数据

也就是说,static注定与差异化数据无关,即与具体对象的数据无关。

以静态方法为例,当你确定一个方法只提供通用的操作流程,而不会在内部引用具体对象的数据时,你就可以把它定为静态方法。

一贯的解释都是上来就告诉你静态方法不能访问实例变量,再解释为什么,是倒着解释的。而上面这段话的出发点是,当你满足什么条件时,你就可以把一个方法定为静态方法。

比如在Python中定义了一个方法:

def simple_print(self):
    print("方法中不涉及具体的对象数据,啦啦啦啦~")

IDE发现你并没有操作具体的对象数据,是一个通用的操作,于是提醒你这个方法可以用static。

要解决这个警告,有两种方式:

  • 在方法中引用对象的数据,变成实例方法

  • 坚持不在方法内使用对象引用,但把它变成静态方法

发现:抽取成静态方法后,形参不需要self了,Python在调用这个方法时也不再传递当前对象,反正静态方法是不处理特定对象数据的

可以反过来解释为什么Java中静态方法无法访问非静态数据(实例字段)和非静态方法(实例方法):因为Java不会在调用静态方法时传递this,静态方法内没有this当然无法处理实例相关的一切。

我们在一个实例方法中调用另一个实例方法或者实例变量时,其实都是通过this调用的,比如

public void test(this){

System.out.println(this.name);

this.show();

}

只不过Java允许我们不显示书写。

示例

public class Demo {

    public static void main(String[] args) {
        /**
         * new一个子类对象
         * 我们知道,子类对象实例化时,会隐式调用父类的无参构造
         * 所以Father里的System.out.println()会执行
         * 猜猜打印的内容是什么?
         */
        Son son = new Son();

        Daughter daughter = new Daughter();
    }

}

class Father{
    public Father(){
        // 打印当前对象所属Class的名字
        System.out.println(this.getClass().getName());
    }
}

class Son extends Father {
}

class Daughter extends Father {
}

打印出子类Son、Daughter的名字。

子类实例化时会隐式调用父类的构造器,效果相当于这样:

class Father{
    /**
     * 父类构造器
     */
    public Father(){
        // 打印当前对象所属Class的名字
        System.out.println(this.getClass().getName());
    }
}

class Son extends Father {
    public Son() {
        // 显示调用父类无参构造
        super();
    }
}

调用构造器,其实也是调用方法,只不过构造器比较特殊。但可以肯定,这个过程中一定也会传递this。Python的构造器就是传递self:

# 构造函数,变量前面下划线,是访问修饰符,表示私有
def __init__(self, name, age):
    self.__name = name
    self.__age = age

本质和子类调用方法给父类传参一样的,只不过传参的过程很特殊:

  • new的时候自动传参,不是我们主动调用,所以感知不到
  • Java中的this是隐式传递的,所以我们更加注意不到了

静态方法:最接近全局的方法

调用静态方法/变量需要用类名调用,只是在本类中可以省略

某个动作在执行时需要对象的参与,这个方法应该定义为实例方法,例如:人可以学习,但是不同的人学习效果是不同的,显然是学习这个动作存在对象差异化,该方法应该定义为实例方法

在Java中没有任何东西是全局(global)的,但可以这样想:如果一个方法不依靠实例变量的值,例如Math类的round()方法,它永远都执行相同的工作:取出浮点数(方法的参数)的最接近的整数值。假定有10000个Math实例,并且都 执行round(42.2),所得到的值永远都是42。换句话说,这个方法会对参数进行操作,但是这个操作不受实例变量状态的影响,唯一能改变其行为的是传入的参数。(静态方法不需要对象的参与)

实际上,永远都无法创建Math的实例,在Math这个类中所有方法都不需要实例变量,所以无需创建Math的实例,会用到的只有Math这个类本身。

int x = Math.round(42.2);
int y = Math.min(56,12);
int z = Math.abs(-343);

这些方法不需要实例变量,因此也不需要特定对象来判别行为。

非静态方法:实例变量会影响到该方法的行为

内存分析static

static方法中是没有aload_0加载this指针的,编译器不会在调用static方法时传递this,所以无法处理实例相关的数据

super

  1. super.属性名 访问父类当中的属性

  2. super.方法名(实参) 访问父类当中的方法

  3. super(实参) 调用父类构造方法

super和this很相似,但是严格来说super并不是一个引用,只是一个关键字,super代表了当前对象中从父类继承过来的特征,this指向一个独立的对象,而super并不是指向某个“独立对象”

  • super只能出现在实例方法 super. 或者构造方法 super()
  • super. 大部分情况下是可以省略的
  • super不能用在静态方法中
  • super()只能出现在构造方法第一行,通过当前的构造方法调用父类中的构造方法,目的是:创建子类对象时初始化父类特征

重要结论:

当一个构造方法的第一行既没有this()也没有super()的时候,默认会有一个super(); 表示通过当前子类的构造方法调用父类的无参数构造方法,所以必须保证父类的无参构造是存在的

注意:this()和super()不能共存,他们都是只能出现在构造方法第一行(引申来说 不管构造方法如何实现,父类的构造方法一定会执行

目的:父类构造方法一定执行是为了对父类型特征进行初始化,子类中有可能访问父类中的数据,如果不进行初始化,子类就不能进行使用。

对父类变量的初始化也是遵循构造方法的执行流程的。

super内存结构

在构造方法执行的时候一并调用了父类的构造方法,父类构造方法又调用了Object的构造方法,但实际上对象只有一个

super(实参)的作用是:初始化当前对象的父类型特征,并不是创建新对象,实际上对象只创建了一个,最终这部分特征都是要继承过来的(super和当前对象紧密相连,静态相关中不能出现super)

public class SuperMemoryStructure {
    public static void main(String[] args) {
        CreditAccount ca1 = new CreditAccount();
        System.out.println(ca1.getActno() + " , " + ca1.getBalance() + " , " + ca1.getCredit());
        CreditAccount ca2 = new CreditAccount("111",100.0,0.99);
        System.out.println(ca2.getActno() + " , " + ca2.getBalance() + " , " + ca2.getCredit());
    }
}
class Account extends Object{
    private String actno;
    private double balance;

    public Account() {
    }

    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

}
class CreditAccount extends Account{
    private double credit;

    public CreditAccount(String actno, double balance, double credit) {
        super(actno, balance);
        this.credit = credit;
    }
    /**
     * 此处不能写 this.actno = actno 因为私有属性只能本类中访问
     * 或者可以用set/get方法
     * */
    public double getCredit() {
        return credit;
    }

    public void setCredit(double credit) {
        this.credit = credit;
    }

    public CreditAccount() {
    }
}

内存结构如下:

属性隐藏

当父类和子类中定义了同名的属性时,super.调用的就是父类中的属性,this.调用的就是子类中的属性

public class PropertyIsHidden {
    public static void main(String[] args) {
        new Vip("lisa").shopping();
    }
}
class Customer{
    String name;

    public Customer() {
    }

    public Customer(String name) {
        this.name = name;
    }
}
class Vip extends Customer{
    String name;

    public Vip(String name) {
        super(name);
        //Vip中的name被赋默认值null
    }
    public void shopping(){
        System.out.println(this.name + " shopping");
        System.out.println(super.name + " shopping");//输出结果说明了,super.name和this.name是不同的存储空间
        System.out.println(name + " shopping");
    }
}

或者可以这样做:

    public void shopping(){
        System.out.println(super.name + " shopping");
        System.out.println(((Customer)this).name + " shopping");
    }
// Customer c = (Customer)this;

将子类强转为父类型,这种做法多用于多层继承的结构当中。

如何理解super

  • 在属性隐藏使访问父类成员变量a
super.a = 30;
((A)this) = 30;

字节码文件相同,都是:

aload_0
bipush   30
putfield  #7   //Field test1/A.a:I

就是找A.a的偏移量而已,结果是12

  • 访问子类中的a
this.a = 40;
((B)this).a = 40;

字节码文件相同:

aload_0
bipush   40
putfield  #11  //Field a:I

就是查找B.a的偏移量而已

多态

多态属于面向对象三大特征之一,前提是封装形成独立体,独立体之间形成继承关系,从而产生多态机制;

多态是同一个行为具有多个不同表现形式或者形态的能力。

多态指的是:编译阶段绑定父类中的方法,运行阶段动态绑定子类中的方法(编译时一种形态,运行时一种形态)

Animal a1 = new Animal();
a1.move();//Animal move
Cat c1 = new Cat();
c1.move();//Cat wondering
Bird b1 = new Bird();
b1.move();//Bird flying

Animal和Cat有继承关系,Cat is a Animal,java中支持语法:父类型引用指向子类型对象,即:

Animal a = new Cat()

a就是父类型引用, new Cat() 就是子类型对象。

Animal a2 = new Cat();
a2.move();//Cat wondering
Animal a3 = new Bird();
a3.move();//Bird flying

分析上述代码:

  1. 编译阶段:对于编译器来说,只知道a2的类型是Animal,在运行时会去Animal虚方法表中查找move方法(和继承结构向上),找到了就会绑定上move方法,静态绑定成功
  2. 运行阶段:实际上在堆内存中创建的java对象是Cat,所以move的时候真正参与运行的是Cat对象,执行阶段就会动态绑定Cat类虚方法表中的move方法

运行期的绑定流程:

  1. 找到操作数栈顶第一个元素指向的对象的实际类型,记作C。
  2. 如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限验证,如果通过则返回这个方法的直接引用,查找结束;不通过返回java.lang.IllegalAccessError异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,抛出java.lang.AbstractMethodError异常。

Java中允许:

  • 向下转型:父类型引用指向子类型对象
  • 向上转型:父类型引用转换为子类型引用

如果要调用子类中特有的方法:

Animal animal = new Cat();  
animal.catchMouse(); // 报错

编译就会报错,因为编译阶段无法在Animal的虚方法表(及其向上继承结构)中查找到catchMouse(),静态绑定失败。

也就是说:子类中的方法无法静态绑定,如果想要访问子类特有的方法,必须向下转型。

将Animal类型的引用强制转型为Cat类型:

Animal animal = new Cat();  
Cat cat = (Cat) animal;  
cat.catchMouse();

但是向下转型是存在风险的:ClassCastException

Animal a6 = new Bird();
Cat cat1 = (Cat)a6;//这时编译器检测到a6是Animal类型,和Cat有继承关系,可以向下转型
cat1.catchMouse();//堆内存中实际创建的对象是Bird 实际运行过程中Bird转换为Cat就不行了 没有继承关系

编译器检测到a6是Animal类型,和Cat类型有继承关系,可以向下转型。但是在运行期间,堆内存中实际创建的对象是Bird,这样转换就不行了,Bird和Cat并没有继承关系。

在堆内存创建的是Bird对象,Animal类型引用可以访问到Bird对象中继承自Animal的部分内容,但是如果将Animal类型引用转型为Cat类型引用,Cat类型引用是无法访问到Bird对象中的内容的,这就是破坏了引用的访问能力;而编译器不知道a6具体指代的是哪个对象,检测到Animal和Cat没有继承关系就不会有错误提示,只有在运行阶段,JVM发现堆内存中实际是Bird对象,想要转型就会发生:ClassCastException

避免类型转换异常的发生:instanceof运算符,在运行阶段动态绑定引用指向对象的类型 结果只能是true或false

	Animal animal = new Cat();  
	if (animal instanceof Cat){  
	    ((Cat) animal).catchMouse();  
	}

新特性:如果判断成功直接转型:

	Animal animal = new Cat();  
	if (animal instanceof Cat cat){  
	    cat.catchMouse();  
	}

具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

为什么要使用instanceof运算符

public class InstanceofTest01 {
    public static void main(String[] args) {
        AnimalTest at = new AnimalTest();
        at.test(new Bird());
        at.test(new Cat());
    }
}
class AnimalTest{
    public void test(Animal a){
        if(a instanceof Cat c){
            c.catchMouse();
        } else if (a instanceof Bird b) {
            //Bird b = (Bird) a;
            b.getClass();
        }

    }
}

在test中,可能传过来的是Bird 也可能传过来的是Cat 就需要进行判断

字段没有多态性

有继承结构如下:

class Animal{
    String name = "动物";
    public void show(){
        System.out.println("Animal-show()");
    }
    public void doSome(){
        System.out.println("AnimalName:" + this.name);
    }
}
class Dog extends Animal{
    String name = "狗";

    @Override
    public void show() {
        System.out.println("Dog-show()");
    }

    @Override
    public void doSome() {
        System.out.println("DogName:" + this.name);
    }
}
class Cat extends Animal{
    String name = "猫";

    @Override
    public void show() {
        System.out.println("Cat-show()");
    }
    @Override
    public void doSome() {
        System.out.println("CatName:" + this.name);
    }
}
  • 直接访问实例变量:输出的就是左边类型中的内容(属性没有动态绑定的能力
    public static void main(String[] args) {
        Animal a = new Dog();
        System.out.println(a.name);//动物
    }

多态性的根源在于虚方法的调用指令invokevirtual的执行逻辑,那自然可以得出结论,多态只对方法有效,对字段是无效的,因为字段的访问并不使用这条指令。事实上,Java中只有虚方法存在,字段是永远不可能变为虚的,换句话说,字段永远不参与多态。某个类的方法访问某个字段时,这个字段指的就是该类可以看到的那个字段。子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段。

如果是用子类引用Dog类型d变量访问name,会先去子类的内存空间中查找,如果没有再访问父类型特征当中的变量

  • 通过实例方法访问成员变量:输出的是属于本方法的成员变量
    public static void main(String[] args) {
        Animal a = new Dog();
        a.doSome();//DogName:狗
    }

多态的优势和弊端

在多态的形式下,右边的对象可以解耦合,便于扩展和维护。

定义方法的时候,使用多态作为参数,可以接收所有的子类对象。

例如StringBuilder类的append方法:

public StringBuilder append(Object obj) {
    return append(String.valueOf(obj));
}

valueOf:

public static String valueOf(Object obj) {
    return (obj == null) ? "null" : obj.toString();//valueOf调用了toString方法
}

继承与多态

当定义出一组类的父型时,可以用子类的任何类型来填补任何需要或期待的父型的位置

Animal[] animals = new Animal[5];

animals[0] = new Dog();
animals[1] = new Cat();
animals[2] = new Wolf();
animals[3] = new Hippo();
animals[4] = new Lion();

for(int i = 0; i < animals.length; i++){
    animals[i].eat();
    animals[i].roam();
}

编译器会查找引用类型来决定是否可以调用该方法,但在执行期间,编译器寻找的不是引用的类型而是堆上的对象,假定此时编译器同意这个调用,则唯一能通过的方法是覆盖的方法也具有相同的参数和返回值。

并且与上例相同 参数与返回值类型也可以是多态

多态存在的三个条件:

  1. 继承
  2. 方法覆盖
  3. 父类型引用指向子类型对象

多态显然是离不开方法覆盖机制的,多态就是因为编译时绑定的父类中的方法,运行时实际上绑定的子类的方法,如果子类对象的方法没有重写,这个时候创建子类对象就没有意义了,只有子类将方法重写之后调用到子类对象上的方法产生不同效果时,多态就形成了。

三个问题

问题一:静态方法没有方法覆盖

public class StaticMethodOverWrite {
    public static void main(String[] args) {
        Math m = new Math();
        Math s = new MathSubClass();
        m.sum();
        s.sum();
    }
}
class Math{
    public static void sum() {
        System.out.println("Math sum execute");
    }
}
class MathSubClass extends Math{
    public static void sum() {
        System.out.println("MathSubClass sum execute");
    }
}

这就说明 静态方法执行的时候跟对象无关,虽然是引用.调用静态方法,但在执行的时候会变成类名.就会执行Animal的sum方法

静态方法的调用是通过invokestatic字节码指令来完成的,仅与对象的静态类型有关。

问题二:私有方法不能覆盖

public class PrivateMethodOverWrite {
    private void doSome(){
        System.out.println("Private doSome execute");
    }

    public static void main(String[] args) {
        PrivateMethodOverWrite privateMethodOverWrite = new T();
        privateMethodOverWrite.doSome();//此处执行的是子类中的方法 ,说明父类中私有方法没有继承
    }
}
class T extends PrivateMethodOverWrite{
    public void doSome(){//此处重写需要注意:访问权限不能更低,只能更高或者持平
        System.out.println("SubClass doSome execute");
    }
}

只有在虚方法表中的方法才能被覆盖,私有方法的方法引用在编译期就已经确定。

问题三:不能抛出更多/更宽泛的异常

重写之后的方法不能比重写之前的方法抛出更多 / 更宽泛的异常

class Animal{
    public void doSome(){
        
    }
}
class Cat extends Animal{
    @Override
    public void doSome() throws Exception{
        super.doSome();
    }
}

编译报错,原因在于:编译器检测到的父类是没有异常抛出的,而在子类执行期间却抛出了编译时异常

但是对于RunTimeException来说:

class Animal{
    public void doSome(){

    }
}
class Cat extends Animal{
    @Override
    public void doSome() throws RuntimeException{
        super.doSome();
    }
}

这是不会报错的,因为不要求对运行时异常进行处理

兼容的返回值类型

public class CompatibleReturnValue {
    public Animal doSome(){
        return null;
    }
}
class SubClass extends CompatibleReturnValue{
    @Override
    public Cat doSome() {//返回值类型变为父类方法中声明的子类 可行
        return null;
    }
}

兼容的返回值类型:子类的返回值类型必须是相同的类型或者是该类型的子类

不能破坏引的访问能力(也就是访问到的内容)

多态的作用

public class PolymorphismInActualDevelopment {
    public static void main(String[] args) {
        new Master(new Cat()).feed();
        new Master(new Bird()).feed();
        new Master(new Dog()).feed();
    }
}
class Master{
    Animal a;
    public Master(Animal a) {//Animal a = new Cat();
        this.a = a;
    }
    public void feed(){
        a.eat();
    }
}
class Dog extends Animal{
}

练习

需求:根据需求完成代码:
	1.定义狗类
		属性:
			年龄,颜色
		行为:
			eat(String something)(something表示吃的东西)
			看家lookHome方法(无参数)
2.定义猫类
	属性:
		年龄,颜色
	行为:
		eat(String something)方法(something表示吃的东西)
		逮老鼠catchMouse方法(无参数)
3.定义Person类//饲养员
	属性:
		姓名,年龄
	行为:
		keepPet(Dog dog,String something)方法
			功能:喂养宠物狗,something表示喂养的东西
	行为:
		keepPet(Cat cat,String something)方法
			功能:喂养宠物猫,something表示喂养的东西
	生成空参有参构造,set和get方法  
4.定义测试类(完成以下打印效果):
	keepPet(Dog dog,String somethind)方法打印内容如下:
		年龄为30岁的老王养了一只黑颜色的2岁的狗
		2岁的黑颜色的狗两只前腿死死的抱住骨头猛吃
	keepPet(Cat cat,String somethind)方法打印内容如下:
		年龄为25岁的老李养了一只灰颜色的3岁的猫
		3岁的灰颜色的猫眯着眼睛侧着头吃鱼
5.思考:		
	1.Dog和Cat都是Animal的子类,以上案例中针对不同的动物,定义了不同的keepPet方法,过于繁琐,能否简化,并体会简化后的好处?
	2.Dog和Cat虽然都是Animal的子类,但是都有其特有方法,能否想办法在keepPet中调用特有方法?

final

final表示最终的、不可变的; final可以修饰类、变量、方法等

  • final修饰的类无法继承 final class A
  • final修饰的方法无法覆盖 public final void doSome()
  • final修饰的变量只能赋一次值 final int i

其实final本质上就做一件事:把动态的变成静态的,把不确定的变成确定的。以final method为例,当一个方法被final修饰,那么子类就不允许重写了,所以obj.method()调用时就是确定的。

final修饰的变量

局部变量

final修饰的局部变量一旦赋值不能重新赋值;多用于多线程并发的环境下,多个线程需要操作这个共享的数据,而这个共享数据涉及到写操作时会导致线程不安全,为了保证数据的安全性可以用final修饰该变量,这样的变量值只允许读不允许写,可以避免线程安全问题。

public class FinalVariable {
    public static void main(String[] args) {
        final int i = 10;
        i = 200; //报错:无法为最终变量i赋值
        
        public void test(final int k){
            k = 20; //一旦赋值就无法修改
        }
    }
}

或者当局部变量作为闭包结构的一部分时,也需要使用final来保证变量一致性

实例变量

final修饰的实例变量 系统不会赋默认值 必须手动赋值实例变量在构造方法执行的时候赋值

class User{
    final double height;
	final int i = 1;
    public User(double height) {
        this.height = height;//这样写也是可以的 在构造的时候进行赋值
    }
}

但是实例变量被final修饰有一个问题:多次创建User对象会导致堆内存中重复出现i的内存空间,既然i是不可变的,初始化一份内存空间所有对象共享就可以了,也就是下面所说的静态变量

静态变量

final修饰的实例变量一般都还会带有static修饰符;static final修饰的变量称为常量,常量名全部大写,单词间下划线衔接,实际上常量和静态变量相同,都存储在静态区并且都是类加载时进行初始化

class Math{
    public static final double PI = 3.14;//常量一般都是公开的 用public修饰
}

出现常量的地方会全部被替换成其记住的字面量,这样可以保证使用常量和直接使用字面量的性能是一样的

final修饰的引用

上文已经说明,final修饰的变量只能赋一次值,而引用也是一个变量;这就说明了:

final修饰的引用只能指向一个对象并且它只能指向一个对象,该变量在整个程序执行过程中不会被GC回收,只有结束后才会被回收

public class FinalVariable {
    public static void main(String[] args) {
        final User u = new User(1);
        u = new User(1);//并且此处也无法让u = null
    }
}

但是对象的细节是可变的

final修饰的类无法继承

final class A{
}
class B extends A{
    /*报错:无法从最终类A进行继承*/
}

不希望被继承:比如字符串类String,String类的方法都是精心设计的,某些方法绝对不能改变,而继承之后重写的方法可能会破坏String类的整体结构

final修饰的方法无法被覆盖

class C{
    public final void doSome(){
        System.out.println("C doSome");
    }
}
class D extends C{
    public void doSome(){
        System.out.println("D doSome");/*报错:被覆盖的方法为final*/
    }
}

不希望被覆盖:某些方法中体现了核心算法,目的是让这个方法受到保护

String

对于String类来说:

final控制了value不能被重新赋值,private导致value不能被外界获取,进而不能修改数组当中的内容

final和static

实际开发中,final和static组合使用的场景居多:

class XxxService {
    // 当我们需要一个 静态常量 时,可以这样写
    private static final int a = 1;
    
    // 省略...
}

public final class ConnectionUtils {
    
    private ConnectionUtils() {}

    // 全局只要一个tl对象,而且final不允许改变
    private static final ThreadLocal<Connection> tl = new ThreadLocal<>();

    private static final BasicDataSource DATA_SOURCE = new BasicDataSource();

    // 对于static final修饰的DATA_SOURCE,希望做一些较为复杂的赋值工作,可以挪到静态代码块
    static {
        DATA_SOURCE.setDriverClassName("com.mysql.jdbc.Driver");
        DATA_SOURCE.setUrl("jdbc:mysql://localhost:3306/demo");
        DATA_SOURCE.setUsername("root");
        DATA_SOURCE.setPassword("123456");
    }
}

final和static单独使用的场景,final表示“不能更改”,static表示“属于类”。

public final class EnumUtil { // 工具类,没必要继承(当然,这玩意可写可不写)
    
}

public void method() {
    final long userId = 1L; // 不希望这个值被后面的语句覆盖(也是可写可不写)
    // ...
    
}

// 如果你有需求,不希望子类覆盖某个方法,要么用private,要么用final,取决于你要不要暴露这个方法

另外,static有个比较特别的用法,用来修饰内部类。一般来说,static是无法修饰class的:

但却可以修饰内部类:

public class UserDTO {
    
    private String name;
    private Department department;
    
    // 比如对于一个Response的TO,内部有个字段需要一个TO表示,且只会在你这个接口里使用,就没必要定义为公共类
    static class Department {
        private String name;
    }
    
}

静态内部类的好处是,外部调用者在new的时候无需实例化外部类:

如果内部类没用static修饰,使用起来就很痛苦:

抽象类

对于Animal继承树来说,这样的写法会很奇怪:

Animal animal = new Animal();

虽然是相同的类型,但是并不存在Animal这种动物,一定要有Animal这个类才能继承和产生多态,但是要限制它的子类才能够被初始化,希望得到的是Cat、Bird等对象,而不是一个Animal对象;也就是说 有些类不应该被实例化

通过标记某个类是抽象类,编译器就知道不能创建这个类的任何实例。

Java API中有很多抽象类,比如GUI的函数库里就有很多,只会对GUI组件下的具体子类做初始化动作

类和类之间具有共同特征,将这些共同特征提取出来形成的就是抽象类;类本身就是不存在的,所以抽象类无法创建对象。

抽象类也属于引用数据类型

语法:[修饰符列表] + abstract + class + 类名 { 类体 }

  • 抽象类无法实例化,是供子类继承的;所以abstract和final是非法的修饰符
  • 抽象类有构造方法,供子类super使用
  • 抽象类不一定有抽象方法,抽象方法一定出现在抽象类中
  • 构造方法、静态方法不能声明为抽象方法
public class AbstractTest01{
    public static void main(String[] args) {
       // Account a = new Account();/*报错:类是抽象的无法实例化*/
    }
}
abstract class Account{
    public Account(String s) {
        //子类super()无法调用无参构造,编译报错
    }
}
//抽象类的子类也可以是抽象类
abstract class SaveAccount extends Account{
    
}
//子类继承抽象类 子类可以实例化对象
class CreditAccount extends Account{
    
}

抽象类的目的只是被继承,除了抽象类可以有静态的成员之外。

抽象方法

抽象方法一定出现在抽象类当中,子类实现抽象类一定要把抽象方法实现了;

这是因为没有任何的通用实现是可行的,对于Animal继承树来说,不同的动物也有不同的eat()方法;抽象方法的意义是就算无法实现出方法的内容,但还是可以定义出一组子型共同的协议;抽象方法没有具体内容,它只是为了标记出多态而存在的,表示在继承树下第一个具体类必须实现出所有的抽象方法,当然也可以使用抽象机制把实现的负担再次转移给下层

public class AbstractMethod {
    public static void main(String[] args) {
        Animal a = new Cat();//面向抽象编程
        a.move();
        //抽象类 可以继承抽象方法,不能new对象
        //非抽象类 必须覆盖抽象方法
    }
}
abstract class Animal{
    public abstract void move();
}
class Cat extends Animal{
    @Override
    public void move() {
        System.out.println("Cat wondering");
    }
}
abstract class Bird extends Animal{
    
}

抽象类的细节

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

  1. 抽象类中,可以有构造方法,是供子类创建对象时,初始化父类成员使用的。

理解:子类的构造方法中,有默认的super(),需要访问父类构造方法。

  1. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。例如适配器模式。

  1. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则子类也必须定义成抽象类,编译无法通过而报错。

理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

  1. 抽象类存在的意义是为了被子类继承。

理解:抽象类中已经实现的是模板中确定的成员,抽象类不确定如何实现的定义成抽象方法,交给具体的子类去实现。

抽象类存在的意义是为了被子类继承,否则抽象类将毫无意义。抽象类可以强制让子类,一定要按照规定的格式进行重写。

接口

之前的Animal继承树:

如果现在想在其中加上宠物特有的功能,可以这样做:

方法一

最简单的做法是把宠物方法加入Animal类当中

优点是 所有动物都可以立刻继承宠物的行为,不需要改变子类程序的代码,新增加的动物会自动获得这些功能

缺点在于 Hippo、Wolf这些动物不会表现出宠物的行为

方法二

采取方法一,但是把宠物的功能设定为抽象的,强迫每个子类进行覆盖

优点:可以让非宠物的动物在覆盖这些方法时做出合理的动作或者什么都不做

缺点:所有具体动物都要实现宠物的行为,这样浪费时间;并且这种结构不理想,最重要的是这会让Animal的定义变得有局限性,反而更难让其他类型的程序重复利用

方法三

把方法加到需要的地方;也就是写在需要的类中

优点:只有宠物类才会有宠物的行为

缺点:失去了合约保证,无法确定执行的时doFriendly()还是beFriendly();其次多态无法实现,因为Animal没有共同的宠物行为

所以真正需要的方法是:

  1. 一种可以让宠物行为只应用在宠物身上的方法
  2. 一种确保所有宠物的类都有相同的方法定义的方法
  3. 一种可以运用到多态的方法

这样看起来需要用到两个父类:

但是这样的多重继承Java并不支持,因为会有“致命方块”的问题:

对于ComboDrive调用burn方法的时候到底要运行哪个版本?这必须要有某种规则来处理可能出现的模糊性,额外的规则意味着必须同时学习这些规则与观察适用这些规则的特殊情况,Java基于简单化的原则不允许这种致命方块的出现

示例二:

接口

接口解决致命方块问题其实很简单,把所有方法设置为抽象的,子类必须实现该方法

public class Dog extends Canine implements Pet{} 

实现关系用like – a描述,Dog is a Canine & Dog like a Pet

接口并不是真正的继承,因为无法在其中实现程序代码;接口的作用就是实现多态;如果以接口取代具体的子类或者抽象的父类作为参数或者返回值,就可以传入任何有实现该接口的东西。使用接口就可以继承超过一个以上的类来源:类可以继承过某个父类,并且实现其他的接口,同时其他的类也可以实现同一个接口。因此就可以为不同的需求组合出不同的继承层次

一个类作为多态类型使用时,相同的类型必定来自同一个继承树,而且必须是该多态类型的子类,定义为Canine类型的参数可以接受Wolf或者Dog,但无法接收Cat或Hippo,但用接口来作为多态类型时,对象就可以来自任何地方了,唯一的条件是该对象必须是来自有实现此接口的类。允许不同继承树的类实现共同的接口对Java API来说非常重要。

接口细节

  • 接口是一种引用数据类型。接口是完全抽象的,抽象类是半抽象的(也就说明接口中的内容必须完全抽象)
  • [修饰符列表] + interface + 接口名{}
  • 接口编译后也是一个.class文件
  • 接口支持继承,多继承
  • 接口中只有两部分内容:常量、抽象方法、(默认、静态、私有)
  • 抽象方法定义时 public abstract可以省略
  • 常量声明时public static final可以省略
  • 接口没有构造方法 无法实例化对象

JDK8更新:接口中可以定义默认方法、静态方法 JDK9更新:接口中可以定义私有方法

接口支持多继承

interface A{}
interface B{}
interface C extends A{}
interface D extends A,B //继承了A和B中的抽象方法 

前缀可以省略

interface MyMath{
    public static final double PI = 3.14;
    double Pi = 3.14;
    
    public abstract int sum(int a,int b);//接口中的方法不能带有方法体
    int sub(int a,int b);
}

类实现接口

class MyMathImpl implements MyMath{
    @Override
    public int sum(int a, int b) {
        return a + b;
    }

    @Override
    int sub(int a, int b) {/*报错:正在尝试分配更低的访问权限*/
        return a - b;
    }
}

在覆盖接口中的方法时,修饰符列表不能省略,因为 缺省 < public,而方法覆盖之后存取权限不能变低。

接口可以用来声明一个变量,例如定义一个接口:

public interface Usb {
    void read();
    void write();
}
public interface Wireless {
    void connect();
}

接口的实现类:

public class Kingston implements Usb,Wireless{
    public void read() {
        System.out.println("Kingston usb read");
    }
    public void write() {
        System.out.println("Kingston usb write");
    }
    public void connect() {
        System.out.println("WIFI connected");
    }
}

测试类:

public class KingstonTest {
    public static void main(String[] args) {
        Usb u = new Kingston();
        u.read();
        u.write();
    }
}

一个类可以实现多个接口