目录
1.多态
1.1概述
多态的前提?
- 有继承/实现关系;存在父类引用子类对象;存在方法重写
public class Animal {
String name = "Animal名称";
public void cry(){
System.out.println("动物会叫!!");
}
}
public class Cat extends Animal{
String name = "Cat名称";
@Override
public void cry() {
System.out.println("猫喵喵喵的叫~~~");
}
}
public class Dog extends Animal{
String name = "Dog名称";
@Override
public void cry() {
System.out.println("狗汪汪汪的叫~~");
}
}
public class Test {
public static void main(String[] args) {
//目标:认识多态,搞清楚多态的使用前提
//1.对象多态 行为多态
//Cat a1 = new Cat();//原来的写法
Animal a1 = new Cat();
a1.cry();//方法:编译看左边,运行看右边
System.out.println(a1.name);//成员变量:编译看左边,运行也看左边
Animal a2 = new Dog();
a2.cry();//方法:编译看左边,运行看右边
System.out.println(a2.name);
}
}
方法:编译看左边,运行看右边
成员变量:编译看左边,运行也看左边(变量没有多态性)
注意:多态是对象、行为的多态,java中的属性(成员变量)不谈多态
1.2多态的好处与不足
- 在多态的形式下,右边的对象是解耦合的,更便于扩展和维护。
People p1 = new Student();
p1.run()
例如在这段代码中,如果想把后面的业务修改走Teacher的run(),直接修改上面的对象改为
People p1 = new Teacher();即可
- 定义方法时,使用父类类型的形参,可以接受一切子类对象,扩展性更强、更便利。
public class Test {
public static void main(String[] args) {
//目标:搞清楚使用多态的好处。
//1.多态下右边的对象是解耦合的
Animal a = new Dog();
a.cry();
Cat c = new Cat();
go(c);
Dog d = new Dog();
go(d);
}
//2.多态下,父类类型作为方法的形参,可以接收一切子类对象,方法更便利
public static void go(Animal c){
System.out.println("开始");
c.cry();
System.out.println("结束");
}
}
这个原理叫做对象回调,new Dog()把对象送给参数Animal c然后当方法执行到c.cry()时,又回调Dog()对象中的cry()方法。
但是多态下存在一个问题,不能直接调用子类独有的功能
1.3 多态下的类型转换问题
- 自动类型转换:父类 变量名 = new 子类();
- 强制类型转换:子类 变量名 = (子类)父类变量
- 类型转换是为了能够调子类独有的一些功能
package com.itheima.d3_polymorphism;
public class Test {
public static void main(String[] args) {
//目标:搞清楚多态下的类型转换问题
//1.多态下右边的对象是解耦合的
Animal a = new Dog();
a.cry();
//强制类型转换,可以解决多态下不能直接调用子类独有方法的问题
Dog d1 = (Dog) a;
d1.lookDoor();
//强制类型转换的注意事项:1.只要有继承或者实现多态关系的两个类就可以强制转换,
//编译阶段不会报错(写代码阶段),但是运行时有可能出现强制类型转换异常
//Cat c1 = (Cat) a;//编译不报错,运行报错,类型转化异常
}
//2.多态下,父类类型作为方法的形参,可以接收一切子类对象,方法更便利
public static void go(Animal a){
System.out.println("开始");
a.cry();//对象回调
//因此java建议强制转换前,先判断对象的真实类型,再进行强制转换
if(a instanceof Dog){
Dog d2 = (Dog) a;
d2.lookDoor();
} else if (a instanceof Cat) {
Cat c2 = (Cat) a;
c2.catchFish();
}
System.out.println("结束");
}
}
java建议强制转换前,先判断对象的真实类型,再进行强制转换:对象 instanceof 类型
会返回true或者false
1.4典型例题(一)
class COne {
public void f() {
System.out.println("COne.f");
}
}
class CTwo extends COne {
public void f() {
System.out.println("CTwo.f");
}
}
class CThree {
public void g(COne one) {
System.out.println("g(Cone)");
one.f();
}
public void g(CTwo two) {
System.out.println("g(Ctwo)");
two.f();
}
}
public class Main {
public static void main(String[] args) {
COne one = new CTwo();
CThree three = new CThree();
three.g(one);
}
}
// 输出结果:
// g(Cone)
// CTwo.f
一、变量类型与对象实例的区别
在 Java 中,变量有两种类型:
- 编译期类型(声明类型):由变量定义时的类型决定
- 运行期类型(实际类型):由
new
关键字后的类决定
COne one = new CTwo(); // 编译期类型: COne, 运行期类型: CTwo
内存模型示意图:
栈内存(Stack) 堆内存(Heap)
+-------+ +-----------+
| one | --------> | CTwo对象 |
+-------+ +-----------+
COne类型 包含:
- 继承的属性/方法
- 重写的f()方法
二、方法重载 (Overload) 的绑定机制
方法重载是静态绑定(编译期确定),根据参数的编译期类型选择调用方法:
CThree three = new CThree();
three.g(one); // 参数"one"的编译期类型是COne
编译期解析过程:
-
编译器检查
CThree
类中所有名为g
的方法 -
找到参数类型为
COne
的方法g(COne one)
-
生成调用指令
g(COne)
,忽略运行期实际类型
三、方法重写 (Override) 的调用机制
方法重写是动态绑定(运行期确定),根据对象的实际类型调用方法:
public void g(COne one) {
one.f(); // one的实际类型是CTwo
}
运行期调用过程:
-
JVM 检查
one
引用指向的实际对象类型(CTwo) -
在 CTwo 类中查找是否有重写的
f()
方法 -
调用
CTwo.f()
,输出CTwo.f
四、对比实验验证
通过以下代码可以验证参数类型和方法调用的绑定机制:
public static void main(String[] args) {
COne one = new CTwo();
CTwo two = new CTwo();
CThree three = new CThree();
three.g(one); // 编译期类型COne -> 调用g(COne)
three.g(two); // 编译期类型CTwo -> 调用g(CTwo)
}
输出结果:
g(Cone)
CTwo.f
g(Ctwo)
CTwo.f
五、常见误区分析
-
为什么不调用
g(CTwo two)
?-
方法重载由编译期类型决定,与实际对象类型无关
-
即使
one
实际指向 CTwo 对象,但编译器只知道它是 COne 类型
-
-
如果 CTwo 没有重写 f () 方法会怎样?
class CTwo extends COne { // 没有重写f()方法 }
输出结果会变为:
g(Cone) COne.f
因为实际对象 (CTwo) 中没有 f () 方法,会向上调用父类 COne 的 f () 方法
六、关键知识点总结
机制 | 绑定时机 | 依据 | 示例 |
---|---|---|---|
方法重载 | 编译期 | 参数的声明类型 |
|
方法重写 | 运行期 | 对象的实际类型 |
|
多态 | 运行期 | 父类引用指向子类 |
|
这种编译期和运行期的分离机制,正是 Java 多态性的核心体现。通过这种方式,Java 既保证了类型安全性(编译期检查),又实现了代码灵活性(运行期动态调用)。
1.5典型例题(二)
下列代码的输出结果是什么
class Person {
String name="person";
public void shout(){
System.out.println(name);
}
}
class Student extends Person{
String name="student";
String school="school";
}
public class Test {
public static void main(String[ ] args) {
Person p=new Student();
System.out.println(p instanceof Student);
System.out.println(p instanceof Person);
System.out.println(p instanceof Object);;
System.out.println(p instanceof System);
}
}
详细错误分析
-
编译期类型检查:
-
p
的编译期类型是Person
-
System
类与Person
类无任何继承关系(System
是java.lang
包中的最终类,继承自Object
,但与Person
无关联) -
Java 编译器禁止对无继承关系的类型使用
instanceof
,因此直接报错
-
-
错误信息:
Error: incompatible types: Person cannot be converted to System
如果要合法地使用 instanceof
,右侧类型必须是左侧变量编译期类型的子类或父类:
-
instanceof
的编译期约束:-
编译器会检查
instanceof
右侧类型是否与左侧变量的编译期类型在继承树上相关 -
若无关(如
Person
和System
),则直接拒绝编译
-
-
运行期类型判断规则:
-
若编译通过,
instanceof
在运行时检查对象的实际类型 -
示例:
Object obj = null; System.out.println(obj instanceof Object); // false(null不是任何类的实例)
-
-
继承关系验证方法:
System.out.println(Student.class.isAssignableFrom(Person.class)); // false System.out.println(Person.class.isAssignableFrom(Student.class)); // true
常见应用场景
instanceof
常用于向下转型前的类型安全检查:
if (p instanceof Student) {
Student s = (Student) p; // 安全转型
System.out.println(s.school); // 访问Student特有的属性
}
2.final关键字
- final关键字是最终的意思,可以修饰(类,方法,变量)
- 修饰类:该类被称为最终类,特点是不能被继承了
- 修饰方法:该方法被称为最终方法,特点是不能被重写了
- 修饰变量:该变量有且仅能赋值一次
工具类就需要加final,因为它不需要被继承
有一些需要保护的数据不希望被更改就可以加final
java的变量有两种:成员变量:静态成员变量
局部变量:方法内,形参,for循环变量,构造器中的变量都是局部变量
public class FinalDemo1 {
//5.final修饰静态成员变量:称为常量
//static final修饰的成员变量今后叫做常量,值只有一个,而且不能被改变
//常量单词建议全部大写,多个单词用下划线链接
public static final String SCHOOL_NAME = "学java的bb";
public static final String SCHOOL_NAME2;
static{
SCHOOL_NAME2 = "java";//只能在这里赋值,不能在main里赋值,必须在被加载的那一刻就赋值
//SCHOOL_NAME2 = "java22";//报错,二次赋值
}
private final String name = "高姑娘";//没有意义!!会导致所有对象的名字都叫高姑娘且不能修改
public static void main(String[] args) {
//目标:掌握final关键字的作用
final double r = 3.14;
buy(0.7);
//SCHOOL_NAME = "JAVA";//报错,二次赋值
}
private static void buy(final double z) {
//z = 0.1 //报错
}
}
//1.final修饰类,类不能被继承
//final class A{}
//class B extends A{}//报错
//2.final修饰方法,方法不能被重写
//class C{
// public final void run(){
// }
//}
//
//class A extends C{
// @Override
// public void run() {//报错
// super.run();
// }
//}
典型例题:
// 以下代码是否有问题?为什么?如何解决?
public class Something {
public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
}
public void addOne(final Other o) {
o.i++;
}
}
class Other {
public int i;
}
-
final
参数的作用-
addOne
方法的参数o
被声明为final
,这意味着参数引用不能被重新赋值,但对象内容可以修改。 -
因此,
o.i++
是合法的,因为它修改的是对象o
的属性,而非重新赋值引用o
。
-
-
执行流程
-
main
方法创建Other
对象o
,其初始值i=0
。 -
调用
addOne
方法时,o.i
被递增为1
。 -
若后续打印
o.i
,将输出1
。
-
3.常量的详解
- 使用了static final修饰的成员变量就被称为常量
- 作用:通常用于记录系统的配置信息
- 注意:常量名的命名规范:建议使用大写英文单词,多个单词使用下划线连接起来
编译后会被宏替换,比起直接使用字面量,性能并不会变差
4.抽象类
4.1概述
注意抽象方法只能由有方法签名不能有方法体
//抽象类:必须使用abstract修饰
public abstract class A {
//抽象方法:abstract修饰,只能有方法签名没有方法体
public abstract void go();
}
public class B extends A{
@Override
public void go() {
}
}
一个类继承抽象类,必须重写完抽象类的全部抽象方法,否则这个类也必须是抽象类!!!
4.2 抽象类的场景和好处
- 父类知道每个子类都要做某个行为,但每个子类要做的情况不一样,父类就定义成抽象方法,交给子类重写实现,我们设计这样的抽象类,就是为了更好的支持多态。
抽象类的好处:方法体代码无意义时可以不写(简化代码),强制子类重写(更好的支持了多态)
最佳实践!!!
4.3 模板方法设计模式
模板方法设计模式解决了什么问题?
- 解决方法中存在重复代码的问题
注意:模板方法是给对象直接使用的,不能被重写,因此要加上final修饰符,保护模板方法。
abstract和final是互斥的关系,一个禁止重写,一个必须重写。
5.接口
5.1概述
我们先来认识一下jdk8之前的接口,有一个大概的认识
定义一个接口A:
//接口
public interface A {
//1.常量:接口中定义常量可以省略public static fianl 不写,默认会加上
//public static final String SCHOOL_NAME = "清华大学";
String SCHOOL_NAME = "清华大学";
//2.抽象方法:接口中定义抽象方法可以省略public abstract不写(简化代码),强制子类重写(更好的支持了多态)
//public abstract void run();
void run();
}
Test:
public class Test {
public static void main(String[] args) {
//目标:认识接口,搞清楚接口的特点
//接口最需要注意的特点:不能创建对象
//A a = new A();//报错
}
}
注意:接口不能创建对象;接口是用来被类实现(implements)的,实现接口的类称为实现类
public class 实现类 implements 接口1,接口2,接口3,....{
}
实现类可以理解为继承接口类的子类,能实现多个接口,但是叫做实现类。
实现类可以实现多个接口,必须重写完全部接口的全部抽象方法,否则这个类必须是抽象类
5.2 接口的好处(重点)
- 弥补了类单继承的不足,一个类同时可以实现多个接口
- 让程序可以面向接口编程,这样程序员就可以灵活方便的切换各种业务实现(更利于程序的解耦合)
接口可以让一个对象有很多个角色,一个角色又可以有很多个对象
5.3 案例
定义了两个实现类都可以完成需求,在Test类中可以挑选所需要的功能进行使用
5.4 接口可以多继承
-
类与类是单继承的:一个类只能直接继承一个父类
-
类与接口是多继承的:一个类可以同时实现多个接口
-
接口和接口是多继承的:一个接口可以同时继承多个接口
//接口多继承可以让实现类只实现一个接口就相当于实现了很多个接口
class D implements A{
@Override
public void a() {
}
@Override
public void b() {
}
@Override
public void c() {
}
}
interface A extends B, C{
void a();
}
interface B{
void b();
}
interface C{
void c();
}
5.4 jdk8之后接口中新增的三种方法
public interface A {
//1.默认方法(普通方法、实例方法):在接口中必须用default修饰
//默认会用public修饰
//这是一个实例方法必须用对象调用,但是接口没有对象,所以必须用接口的实现类的对象来调用
default void run(){
go();
System.out.println("跑的贼快~~~");
}
//2.私有方法(私有的实例方法)(JDK9开始才有的)
//但是私有方法并不能在对象中调用,只能在本类中调用,但是可以在内部的其他实例方法中调用
private void go(){
System.out.println("开始跑~~");
}
//3.静态方法
//默认会用public修饰
//必接口的静态方法必须用接口名本身调用
//接口的实现类不能调用接口类的静态方法,但是普通类中的子类可以通过类名调用父类的静态方法
public static void inAdd(){
System.out.println("我们都在学java");
}
}
为什么要新增这些方法?
- 增强了接口的能力,更便于项目的扩展和维护(如果要在A接口中新增一个方法,直接在接口类A中写一个默认方法,所有的实现类中不需要再次重写方法)
5.5 接口的其他注意事项(了解)
6.接口类型和向下类型的强制转换
核心概念对比
对比项 | 接口类型转换 | 向下转型(Downcasting) |
---|---|---|
语法 | 接口类型 变量 = (接口类型) 对象; | 子类类型 变量 = (子类类型) 父类引用; |
目的 | 让对象通过接口引用调用接口方法 | 让父类引用能调用子类特有的方法 |
前提条件 | 对象必须实现了该接口 | 父类引用实际指向子类对象 |
风险 | 若对象未实现接口,抛出 ClassCastException | 若父类引用未指向目标子类,抛出异常 |
//接口类
interface Flyable { void fly(); }
class Bird extends Animal implements Flyable{
@Override
public void fly() { System.out.println("鸟飞"); }
public void eat() { System.out.println("鸟吃虫"); }
}
//父类
class Animal {
public void eat() { System.out.println("动物进食"); }
}
class Dog extends Animal {
@Override
public void eat() { System.out.println("狗叫"); }
}
接口类型转换
public static void main(String[] args) {
Animal animal = new Bird(); // 父类引用指向子类对象
animal.eat(); // 调用Bird重写的方法
// animal.fly(); 编译错误,需向下转型
// 转换为接口类型,判断当前new的animal对象是否与Flyable有实现关系,相当于判断当前的动物会保护
//会飞,会飞的话进行接口类型转换,输出bird.fly()
if (animal instanceof Flyable) {
Flyable flyable = (Flyable) animal; // 合法:Bird实现了Flyable
flyable.fly(); // 调用接口方法
}
// 错误示例:若animal实际是Dog(未实现Flyable)
animal = new Dog();
flyable = (Flyable) animal; // 运行时抛出ClassCastException
}