1.面向对象概述
1.1面向过程和面向对象
面向过程(Procedural Programming)和面向对象(Object-Oriented Programming,简称 OOP)是两种不同的编程范式。
1.面向对象
面向对象是一种以对象为中心的编程思想。它将程序视为对象的集合,每个对象都有自己的状态(属性)和行为(方法)。在面向对象编程中,数据和操作是紧密相关的,并通过封装、继承和多态等机制实现代码的组织和复用。主要特点包括:
-
1.以对象为核心:程序由多个相互交互的对象组成,每个对象都有自己的属性和方法。
-
2.封装:将数据和相关的操作封装在对象中,通过公共接口来访问和操作数据。
-
3.继承:允许新建类从现有类派生,继承现有类的属性和方法,并可以在此基础上进行修改或扩展。
-
4.多态:允许不同类的对象对同一消息作出不同的响应,提高代码的灵活性和可扩展性。
其中2,3,4也是面向对象的三大特征
2.面向过程
面向过程是一种以过程、函数为中心的编程思想。它将程序视为一系列的步骤或操作,旨在解决问题。在面向过程编程中,数据和操作是分离的,通过函数来处理数据。主要特点包括:
-
1.以函数为核心:程序由一个个独立且顺序执行的函数构成,每个函数完成特定的任务。
-
2.数据和处理分离:数据是被多个函数共享的,函数通过传递参数来处理数据。
-
3.重用性较低:通常情况下,无法将函数复用到其他程序中。
总结:
面向对象编程更适用于大型和复杂的项目,具有可重用、可扩展和易于维护的优势;而面向过程编程则更适用于简单和小规模的任务,具有简单、直观和执行效率高的特点。选择哪种编程方式需要根据具体的需求、项目规模和开发团队的技术水平进行权衡。
1.2 7种原则
-
单一职责原则(Single Responsibility Principle)
每一个类应该专注于做一件事情。 -
里氏替换原则(Liskov Substitution Principle)
超类存在的地方,子类是可以替换的。 -
依赖倒置原则(Dependence Inversion Principle)
实现尽量依赖抽象,不依赖具体实现。 -
接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。 -
迪米特法则(Law Of Demeter)
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。 -
开闭原则(Open Close Principle)
面向扩展开放,面向修改关闭。 -
组合/聚合复用原则(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
区别点 | this | super | |
---|---|---|---|
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);
}
}
- 多态分析
-
多态性,是面向对象中最重要的概念, 在Java中的体现:对象的多态性:父类的引用指向子类的对象
-
多态,提高了代码的通用性,常称作接口重用
- 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。
通过这个示例,我们可以看到,编译器在编译时根据变量的声明类型进行类型检查和方法绑定,而实际调用的方法是由对象的运行时类型决定的。