Bootstrap

java基础03-面向对象

1.面向对象概述

1.1面向过程和面向对象

面向过程(Procedural Programming)和面向对象(Object-Oriented Programming,简称 OOP)是两种不同的编程范式。
1.面向对象
面向对象是一种以对象为中心的编程思想。它将程序视为对象的集合,每个对象都有自己的状态(属性)和行为(方法)。在面向对象编程中,数据和操作是紧密相关的,并通过封装、继承和多态等机制实现代码的组织和复用。主要特点包括:

  • 1.以对象为核心:程序由多个相互交互的对象组成,每个对象都有自己的属性和方法。

  • 2.封装:将数据和相关的操作封装在对象中,通过公共接口来访问和操作数据。

  • 3.继承:允许新建类从现有类派生,继承现有类的属性和方法,并可以在此基础上进行修改或扩展。

  • 4.多态:允许不同类的对象对同一消息作出不同的响应,提高代码的灵活性和可扩展性。

其中2,3,4也是面向对象的三大特征

2.面向过程
面向过程是一种以过程、函数为中心的编程思想。它将程序视为一系列的步骤或操作,旨在解决问题。在面向过程编程中,数据和操作是分离的,通过函数来处理数据。主要特点包括:

  • 1.以函数为核心:程序由一个个独立且顺序执行的函数构成,每个函数完成特定的任务。

  • 2.数据和处理分离:数据是被多个函数共享的,函数通过传递参数来处理数据。

  • 3.重用性较低:通常情况下,无法将函数复用到其他程序中。

总结:
面向对象编程更适用于大型和复杂的项目,具有可重用、可扩展和易于维护的优势;而面向过程编程则更适用于简单和小规模的任务,具有简单、直观和执行效率高的特点。选择哪种编程方式需要根据具体的需求、项目规模和开发团队的技术水平进行权衡。

1.2 7种原则

  1. 单一职责原则(Single Responsibility Principle)
    每一个类应该专注于做一件事情。

  2. 里氏替换原则(Liskov Substitution Principle)
    超类存在的地方,子类是可以替换的。

  3. 依赖倒置原则(Dependence Inversion Principle)
    实现尽量依赖抽象,不依赖具体实现。

  4. 接口隔离原则(Interface Segregation Principle)
    应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。

  5. 迪米特法则(Law Of Demeter)
    又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。

  6. 开闭原则(Open Close Principle)
    面向扩展开放,面向修改关闭。

  7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
    尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。

1.3 类的成员

属 性:对应类中的成员变量
行 为:对应类中的成员方法

1.4 内存解析

在这里插入图片描述

  • 堆(Heap),此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

  • 通常所说的栈(Stack),是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char 、 short 、 int 、 float 、 long 、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法也在栈中存储,方法执行完,自动释放。

  • 方法区(Method Area),用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

下面我们用一个案例来看一下是如何存储的:
在这里插入图片描述
在方法区对Person.class加载后加载到内存中,然后main方法被调用入栈,创建对象p1

这里需要注意在这main方法栈中创建的p1对象只是保存了一个地址,地址是指向堆中的数据

然后我们看标注把名字修改为马云,堆中的数据就被修改为马云

1.5 属性

1.变量的分类

  • 在类中方法外,被声明的变量称为成员变量 还有用static关键字修饰的静态变量
  • 在方法体内部声明的变量称为局部变量

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.6方法

  • 方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。
  • 将功能封装为方法的目的是,可以实现代码重用,简化代码
  • Java里的方法不能独立存在,所有的方法必须定义在类里。

1.6.1 方法的重载

在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。
重载的特点:与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别

使用案例:
编写程序,定义三个重载方法并调用。方法名为mOL。

  • 三个方法分别接收一个int参数、两个int参数、一个字符串参数。分别执行平方运算并输出结果,
    相乘并输出结果,输出字符串信息。
  • 在主类的main ()方法中分别用参数区别调用三个方法。

定义三个重载方法max(),第一个方法求两个int值中的最大值,第二个方法求两个double值中的最大
值,第三个方法求三个double值中的最大值,并分别调用三个方法。

public class OverloadExer {
    //1. 如下的三个方法构成重载
    public void mOL(int i) {
        System.out.println(i * i);
    }

    public void mOL(int i, int j) {
        System.out.println(i * j);
    }

    public void mOL(String s) {
        System.out.println(s);
    }

    //2.如下的三个方法构成重载
    public int max(int i, int j) {
        return (i > j) ? i : j;
    }

    public double max(double d1, double d2) {
        return (d1 > d2) ? d1 : d2;
    }

    public double max(double d1, double d2, double d3) {
        double max = (d1 > d2) ? d1 : d2;
        return (max > d3) ? max : d3;
    }
}

1.6.2可变参数

//JDK 5.0以前:采用数组形参来定义方法,传入多个同一类型变量
public static void test(int a ,String[] books);
//JDK5.0:采用可变个数形参来定义方法,传入多个同一类型变量
public static void test(int a ,String… books)

1.声明格式:方法名(参数的类型名 …参数名)
2. 可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个
3. 可变个数形参的方法与同名的方法之间,彼此构成重载
4. 可变参数方法的使用与方法参数部分使用数组是一致的
5. 方法的参数部分有可变形参,需要放在形参声明的最后
6. 在一个方法的形参位置,最多只能声明一个可变个数形参

public class Test2 {
    public void show(String[] arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
    public void display(String... arr) {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
    public void display(String name, int... arr) {
        System.out.println("name=" + name);
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }
    }
    public static void main(String[] args) {
        Test2 t2 = new Test2();
        String[] strs = new String[] {"a", "b", "c"};
        t2.show(strs);
        System.out.println("--------------");
        String[] strs2 = new String[] {"aa", "bb", "cc"};
        t2.display(strs2);
        System.out.println("--------------");
//可变参数用法
        t2.display();
// t2.show(); //报错
        System.out.println("--------------");
        t2.display("mickey", "a", "b");
		t2.display("mickey", 1, 2, 3);
	}
}

1.6.3参数传递

方法,必须由其所在类或对象调用才有意义。若方法含有参数:

  • 形参:方法声明时的参数
  • 实参:方法调用时实际传给形参的参数值
    Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。
  • 形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参
  • 形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

1. 基本数据类型参数传递

public static void main(String[] args) {
int x = 5;
System.out.println("修改之前x = " + x);
// x是实参
change(x);
System.out.println("修改之后x = " + x);
}
public static void change (int x){
System.out.println("change:修改之前x = " + x);
x = 3;
System.out.println("change:修改之后x = " + x);
}

在这里插入图片描述

方法里的变量属于局部变量是不在堆中的

2. 引用类型参数传递

public static void main(String[] args) {
Person obj = new Person();
obj.age = 5;
System.out.println("修改之前age = " + obj.age);
// x是实参
change(obj);
System.out.println("修改之后age = " + obj.age);
}
public static void change(Person obj) {
System.out.println("change:修改之前age = " + obj.age);
obj.age = 3;
System.out.println("change:修改之后age = " + obj.age);
}
其中Person类定义为:
class Person{
int age;
}

在这里插入图片描述

还是方法区加载后,栈给main方法分配了一个栈帧,在main方法中创建对象后存储了一个关于对象属性值的地址如图

然后main方法没结束时候change方法入栈 ,因为change的参数传入的是 obj对象 也就是从main方法的栈帧中把obj这个对象复制到change方法的栈帧中

在change栈帧中obj属性进行修改为从5修改为3,然后obj这个对象的属性age就被修改为3了

栈帧是在运行时动态分配的,它存在于函数调用时的栈空间中,并在函数调用结束后被销毁。当一个函数被调用时,会在栈上分配一个新的栈帧,并将函数的参数、返回地址和其他必要的信息存储在该栈帧内。栈帧还包括局部变量的存储空间。

栈帧通常包含以下几个重要的组成部分:

  • 返回地址(Return Address):用于指示函数执行完毕后返回到调用它的代码的位置。

  • 函数参数(Function Parameters):用于存储函数调用时传递给函数的参数值。

  • 局部变量(Local Variables):用于存储函数内部声明的局部变量。每个栈帧都有自己的一块局部变量区域。

  • 临时数据(Temporary Data):用于存储函数执行过程中的临时计算结果、中间变量等。

  • 上一个栈帧指针(Previous Stack Frame Pointer):指向调用当前函数的上一个栈帧的指针,用于实现函数调用的嵌套。

栈帧的创建和销毁是由函数的调用和返回机制来管理的。每次函数调用时,都会分配一个新的栈帧;当函数返回时,当前的栈帧将被销毁,控制权回到调用者的位置。

栈帧的使用使得函数之间的数据和控制流能够互相独立,使得程序的执行更加有序和可控。

1.6.4 递归方法

递归方法:一个方法体内调用它自身。

  • 方法递归包含了一种隐式的循环,它会重复执行某段代码,但这种重复执行无须循环控制。
  • 递归一定要向已知方向递归,否则这种递归就变成了无穷递归,类似死循环
public class RecursionTest {
    public static void main(String[] args) {
// 例1:计算1-100之间所有自然数的和
// 方式一:
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        System.out.println(sum)
// 方式二:
        RecursionTest test = new RecursionTest();
        int sum1 = test.getSum(100);
        System.out.println(sum1);
        System.out.println("*****************");
        int value = test.f(10);
        System.out.println(value);
    }

    // 例1:计算1-n之间所有自然数的和
    public int getSum(int n) {// 3
        if (n == 1) {
            return 1;
        } else {
            return n + getSum(n - 1);
        }
    }

    // 例2:计算1-n之间所有自然数的乘积:n!
    public int getSum1(int n) {
        if (n == 1) {
            return 1;
        } else {
            return n * getSum1(n - 1);
        }
    }
}

ps:但要注意栈溢出这个问题

那么如何增加JVM的栈空间呢

要增加 JVM 的栈空间,可以使用 -Xss 参数来指定栈的大小。以下是增加栈空间的几种方法:

1.命令行参数方式:在运行 Java 程序时,使用 -Xss 参数指定栈的大小。例如,使用 -Xss4m 将栈的大小设置为 4MB。
shell

java -Xss4m YourProgram

2.在启动脚本中设置参数:如果您使用的是启动脚本(如 .sh、.bat 文件),可以在脚本文件中添加 -Xss 参数来设置栈的大小。

#!/bin/bash
java -Xss4m YourProgram

3.在 IDE 中设置参数:如果您使用集成开发环境(IDE)运行程序,可以在 IDE 的配置或运行选项中添加 -Xss 参数来设置栈的大小。具体步骤会因不同的 IDE 而有所差异。

需要注意的是,在增加栈空间时,应该谨慎使用较大的值,以避免过度消耗内存资源和降低性能。栈空间设置过大可能会导致栈溢出问题的解决,但也可能带来其他问题。

另外,栈空间的大小也受到操作系统的限制,无法无限制地增加。如果遇到持续出现 StackOverflowError 的情况,建议优化代码逻辑或算法,避免过多的递归调用或减少方法调用层级。

2.面对对象基础

2.1 封装

为什么需要封装?封装的作用和含义?

  • 我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?
  • 我要开车,…
    我们程序设计追求“高内聚,低耦合”。
  • 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
  • 低耦合 :仅对外暴露少量的方法用于使用。

隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想

1.属性封装的案例

class Animal {
    public int legs;

    public void eat() {
        System.out.println("Eating");
    }

    public void move() {
        System.out.println("Moving.");
    }
}

public class Zoo {
    public static void main(String args[]) {
        Animal xb = new Animal();
        xb.legs = 4; //问题:xb.legs = -1000,解决方式将legs属性封装起来,防止乱用
        System.out.println(xb.legs);
        xb.eat();
        xb.move();
    }
}

Java中通过将数据声明为私有的(private),再提供公共的(public)方法:getXxx()**和setXxx()**实现对该属性的操作,以实现下述目的:

  • 隐藏一个类中不需要对外提供的实现细节;
  • 使用者只能通过事先定制好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作;
  • 便于修改,增强代码的可维护性
class Animal {
    private int legs;// 将属性legs定义为private,只能被Animal类内部访问

    public void setLegs(int i) { // 在这里定义方法 eat() 和 move()
        if (i != 0 && i != 2 && i != 4) {
            System.out.println("Wrong number of legs!");
            return;
        }
        legs = i;
    }

    public int getLegs() {
        return legs;
    }
}

public class Zoo {
    public static void main(String args[]) {
        Animal xb = new Animal();
        xb.setLegs(4); // xb.setLegs(-1000);
//xb.legs = -1000; // 非法
        System.out.println(xb.getLegs());
    }
}

2.2 构造函数

构造器的特征:

  • 它具有与类相同的名称
  • 它不声明返回值类型。(与声明为void不同)
  • 不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值

构造器的作用:创建对象;给对象进行初始化

  • 如:Order o = new Order(); Person p = new Person(“Peter”,15);
  • 如同我们规定每个“人”一出生就必须先洗澡,我们就可以在“人”的构造器中加入完成“洗澡”的程序代码,于是每个“人”一出生就会自动完成“洗澡”,程序就不必再在每个人刚出生时一个一个地告诉他们要“洗澡”了。

要注意 在创建对象时候 构造函数就已经使用了

案例

public class Animal {
    private int legs;

    // 构造器
    public Animal() {
        legs = 4;
    }

    public void setLegs(int i) {
        legs = i;
    }

    public int getLegs() {
        return legs;
    }}
  • 根据参数不同,构造器可以分为如下两类:
    • 隐式无参构造器(系统默认提供
    • 显式定义一个或多个构造器(无参、有参)
  • Java语言中,每个类都至少有一个构造器
  • 默认构造器的修饰符与所属类的修饰符一致
  • 一旦显式定义了构造器,则系统不再提供默认构造器
  • 一个类可以创建多个重载的构造器
  • 父类的构造器不可被子类继承

赋值顺序
截止到目前,我们讲到了很多位置都可以对类的属性赋值。现总结这几个位置,并指明赋值的先后顺
序。
① 默认初始化
② 显式初始化
③ 构造器中初始化
④ 通过“对象.属性“或“对象.方法”的方式赋值
赋值的先后顺序:
① - ② - ③ - ④

显示初始化就是指

private String name =“小明”

2.3 JavaBean

所谓javaBean,是指符合如下标准的Java类:

  • 类是公共的
  • 有一个无参的公共的构造器
  • 有属性,且有对应的get、set方法
public class JavaBean {
    private String name; // 属性一般定义为private
    private int age;

    public JavaBean() {
    }

    public int getAge() {
        return age;
    }

    public void setAge(int a) {
        age = a;
    }

    public String getName() {
        return name;
    }

    public void setName(String n) {
        name = n;
    }
}

2.4 this

在Java中,this关键字比较难理解,它的作用和其词义很接近。

  • 它在方法内部使用,即这个方法所属对象的引用;
  • 它在构造器内部使用,表示该构造器正在初始化的对象。

this 可以调用类的属性、方法和构造器

  • 当在方法内需要用到调用该方法的对象时,就用this。
  • 具体的:我们可以用this来区分属性和局部变量。比如:this.name = name;

案例

class Person { // 定义Person类
    private String name;
    private int age;

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

    public void getInfo() {
        System.out.println("姓名:" + name);
        this.speak();
    }

    public void speak() {
        System.out.println(“年龄:” + this.age);
    }
}

this的使用方法汇总
1.代表当前对象:this 代表当前正在调用方法的对象。在方法内部,可以使用 this 来引用该对象的成员变量、方法和构造函数。

2.区分局部变量和实例变量:当方法的参数名或局部变量与实例变量同名时,使用 this 可以明确指示要操作的是实例变量。

示例:


public class Person {
    private String name;

    public void setName(String name) {
        this.name = name;  // 使用 this 引用实例变量
    }
}

3.在构造函数中调用另一个构造函数:如果一个类中有多个构造函数,可以使用 this 关键字在一个构造函数中调用同类的另一个构造函数。

示例:


public class Person {
    private String name;
    private int age;

    public Person() {
        this("John Doe", 30);  // 调用另一个构造函数
    }

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

4.返回当前对象:在某些情况下,可以使用 this 关键字返回当前对象本身。例如,在一个方法链式调用中,可以返回 this 以实现方法的连续调用。

示例:

public class Calculator {
    private int result;

    public Calculator add(int number) {
        result += number;
        return this;  // 返回当前对象
    }

    public int getResult() {
        return result;
    }
}


// 链式调用示例
Calculator calculator = new Calculator();
int sum = calculator.add(5).add(3).getResult();  // 结果为 8

3.修饰符

请添加图片描述

  • private:当前类
  • default:当前包可以访问
  • protected:子类可以访问
    • 在子类中new Parent().protected变量不能够访问
    • 在子类中super.protected变量(方法)能够访问
  • public:所有工程能够访问

4.super 和this

区别点thissuper
1访问属性访问本类中的属性,如果本类没 有此属性则从父类中继续查找直接访问父类中的属性
2调用方法访问本类中的方法,如果本类没 有此方法则从父类中继续查找直接访问父类中的方法
3调用构造器调用本类构造器,必须放在构造 器的首行调用父类构造器,必须 放在子类构造器的首行

总结:
也就是this 都是先在本类中寻找 找不到就调用父类中的
super都是直接调用父类中的

5.多态

多态(Polymorphism)是面向对象编程中的一个重要概念,它允许一个变量引用不同类型的对象,并在运行时动态地调用其特定类型的方法。

多态性使得我们可以按照通用的接口处理不同的对象,而无需关注具体的类型。这提供了代码的灵活性和可扩展性,使得程序更易于理解和维护。

在Java中,多态性通过继承和方法重写来实现。具体来说,当一个父类引用变量指向子类的对象时,通过该父类引用可以调用在父类和子类中声明的方法,并根据对象的实际类型来确定要执行的方法。

案例:
请添加图片描述

1.Animal

package h_multi;

public class Animal {

    public void eat() {
        System.out.println("动物吃饭");
    }

    public void shout() {
        System.out.println("动物叫。。。");
    }

}

2.Dog

package h_multi;

public class Dog extends Animal {

    @Override
    public void eat() {
        System.out.println("狗吃骨头....");
    }

    @Override
    public void shout() {
        System.out.println("狗在汪汪叫....");
    }
}

3.cat

package h_multi;

public class Cat extends Animal {

    @Override
    public void eat() {
        System.out.println("猫吃鱼....");
    }

    @Override
    public void shout() {
        System.out.println("猫在喵喵叫....");
    }
}

4.不用多态实现宠物训练

package h_multi;

public class Master {

    //操作狗的行为(没有使用多态)
   public void func(Dog dog) {
       dog.eat();
       dog.shout();
   }

   //操作猫的行为(没有使用多态)
   public void func(Cat cat) {
       cat.eat();
       cat.shout();
   }

    //狮子,老虎
    //...操作狮子的方法
    //...操作老虎的方法

    public static void main(String[] args) {
        //里氏替换原则——>子类一定能够代替父类
        Master master = new Master();
        Dog dog = new Dog();
        Cat cat = new Cat();
        Tiger tiger = new Tiger();
        master.func(dog);
        master.func(cat);
        master.func(tiger);
    }

}

5.多态宠物训练实现

package h_multi;

public class Master {

    //多态的实现版本
    public void func(Animal animal) {
        animal.eat();
        animal.shout();
    }

    public static void main(String[] args) {
        //里氏替换原则——>子类一定能够代替父类
        Master master = new Master();
        Dog dog = new Dog();
        Cat cat = new Cat();
        Tiger tiger = new Tiger();
        master.func(dog);
        master.func(cat);
        master.func(tiger);
    }

}
  1. 多态分析
  • 多态性,是面向对象中最重要的概念, 在Java中的体现:对象的多态性:父类的引用指向子类的对象

  • 多态,提高了代码的通用性,常称作接口重用

  1. instanceof关键词

判断对象是否属于一个类型,一般在强制类型转换前,进行判断

6.Object类

  • ==:比较内存地址
  • equals:比较内容,但是默认比较内存地址,源码如下:
    • String因为重写了equels,所以String比较内容
public boolean equals(Object obj) {
    return (this == obj);
}

重写equals



@Override 
public boolean equals(Object obj) {
    return this.age == ((Bean) obj).age; }
  • toString():打印时,会打印toString()方法
    • 重写toString()方法,来自定义打印信息
   @Override public String toString() {
    return "Bean{" +
            "name='" + name + '\'' +
            ", age=" + age +
            '}'; }

7.getClass:得到方法区中的类信息对象

在Java中,getClass() 是一个Object类中定义的方法,它返回一个对象的运行时(Runtime)类型。

具体来说,当你调用一个对象的getClass()方法时,它会返回一个Class类型的对象,该对象表示该对象的实际类型。Class 类提供了许多有用的方法,例如获取类的名称、访问类的成员等。

下面是一个示例代码,展示了如何使用getClass()方法:

public class Main {
    public static void main(String[] args) {
        String str = "Hello";
        Class<?> strClass = str.getClass();
        System.out.println(strClass.getName());  // 输出:java.lang.String

        Integer num = 10;
        Class<?> numClass = num.getClass();
        System.out.println(numClass.getName());  // 输出:java.lang.Integer
    }
}

在上述代码中,我们创建了一个字符串对象str和一个整数对象num,然后使用它们各自的getClass()方法获取它们的运行时类型。最后,通过调用getName()方法,我们可以打印出它们的完全限定名(Fully Qualified Name)。

需要注意的是,getClass()方法是继承自 Object 类的公共方法,因此可以应用于任何 Java 对象。

8.自动装箱和拆箱

装箱,基本数据类型转换成包装类,成为装箱

  Integer i1 = 100; 
  int i = 123;
  Integer i2 = new Integer(123);
  Integer i3 = new Integer("456");
//基本数据类型int->对应的包装类->Integer
//float-> Float

拆箱:包装类转换成基本数据类型

  

  Integer i1 = new Integer(123);
    //拆箱
    int i2 = i1.intValue();

    Boolean b2 = new Boolean(false);
    boolean b3 = b2.booleanValue(); //拆箱

9.编译时类型和运行时类型

在编程中,有两个常用的类型概念,分别是编译时类型和运行时类型。

编译时类型(Compile-time Type)也称为静态类型,指的是在编写代码时声明的类型。它是编译器在编译阶段确定的类型,并在编译期进行类型检查。编译时类型决定了可以在编码过程中使用的方法和属性。

运行时类型(Runtime Type)指的是在程序运行时实际具备的对象类型。在运行时,对象可以具有与编译时类型相同的类型,也可以具有编译时类型的任何子类型。Java中的多态性就是基于运行时类型来实现的。

下面是一个示例代码,演示编译时类型和运行时类型的概念:

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog();   // 编译时类型:Animal,运行时类型:Dog
        Animal animal2 = new Cat();   // 编译时类型:Animal,运行时类型:Cat

        animal1.eat();    // 在编译时,animal1 是 Animal 类型,调用的是 Animal 的 eat() 方法。
                          // 在运行时,animal1 是 Dog 类型的对象,实际调用的是 Dog 的 eat() 方法。

        animal2.eat();    // 在编译时,animal2 是 Animal 类型,调用的是 Animal 的 eat() 方法。
                          // 在运行时,animal2 是 Cat 类型的对象,实际调用的是 Cat 的 eat() 方法。
  //这里也有多态的体现
    }
}

class Animal {
    public void eat() {
        System.out.println("Animal is eating.");
    }
}

class Dog extends Animal {
    @Override
    public void eat() {
        System.out.println("Dog is eating.");
    }
}

class Cat extends Animal {
    @Override
    public void eat() {
        System.out.println("Cat is eating.");
    }
}

在上述代码中,Animal 是一个基类,它有一个 eat() 方法。Dog 和 Cat 是 Animal 的子类,它们分别重写了 eat() 方法。在 main() 方法中,我们创建了一个 Animal 类型的对象引用 animal1 和 animal2,但它们的实际运行时类型分别是 Dog 和 Cat。

通过这个示例,我们可以看到,编译器在编译时根据变量的声明类型进行类型检查和方法绑定,而实际调用的方法是由对象的运行时类型决定的。

;