面向过程与面向对象
面向过程
是一种以事件为中心的编程思想,编程的时候把解决问题的步骤分析出来,然后用函数把这些步骤实现,在一步一步的具体步骤中再按顺序调用函数。面向对象
是一种以“对象”为中心的编程思想,把要解决的问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个对象在整个解决问题的步骤中的属性和行为。
面向对象三大特性:
- 多态
- 继承
- 封装
类和对象
-
类
- 类是对现实生活中一类具有共同属性和行为的事物的抽象
- 类是对象的数据类型,类是具有相同属性和行为的一组对象的集合
- 简单理解:类就是对现实事物的一种描述
-
类的组成
- 属性:指事物的特征,例如:手机事物(品牌,价格,尺寸)
- 行为:指事物能执行的操作,例如:手机事物(打电话,发短信)
-
对象:真实存在的具体的个体,万物皆对象
类和对象的关系
- 类:类是对现实生活中一类具有共同属性和行为的事物的抽象
多个对象进行综合抽象的结果
- 对象:是能够看得到摸的着的真实存在的实体
一个对象是一个类的实例
- 简单理解:类是对事物的一种描述,对象则为具体存在的事物
类的定义
-
类的成员
-
成员变量(属性):在类中方法外声明的变量,在创建对象的时候会进行初始化(会有默认值)
-
类变量,使用static关键字修饰的变量
-
实例变量
class Demo{ static int id;//类变量 int age;//实例变量 }
-
-
成员方法(行为)
-
类方法,使用static修饰的方法
-
实例方法
class Demo{ //类方法 public static void say(){ //代码 } //实例方法 public void eat(){ //代码 } }
static
关键字-
有
static
的方法可以通过 类名.方法名 调用 -
无
static
的方法属于实例方法,必须使用创建对象时,使用 对象名.方法名 调用 -
使用对象名也可以调用类方法,但是一般不推荐
-
使用
static
修饰的方法,属于类,不属于具体的某个对象,多个对象共享 -
静态方法中不能直接访问实例变量和实例方法
-
在实例方法中可以直接调用类中定义的静态变量和静态方法
-
-
成员类/接口
静态成员与实例成员的区别
- 静态成员是类的成员,实例成员是对象的成员,静态成员是
在类加载时初始化
,可以被类的所有对象共享,实例成员变量在创建对象时初始化
每个对象都有自己的一份 - 静态成员可以直接通过类名访问,不需要创建对象;实例成员需要通过对象来访问
- 实例成员可以访问静态成员和实例成员,但是静态成员不能直接访问实例成员
- 静态成员可以在静态初始化块中初始化,实例成员需要在构造方法中初始化
-
-
静态初始化
java中静态初始化,在类被加载时执行的初始化方式,用于初始化类的静态变量和执行一些静态代码块
注意:静态代码块在类被加载时只执行一次,且只能访问类的静态变量和方法,静态初始化的执行顺序时按照代码块在类中出现的顺序依次执行的
//格式 static{ //要执行的代码 System.out.println("静态初始化执行") }
-
实例初始化:在创建对象时执行,优先级高于构造方法,在每次创建对象时都会执行
{ //代码块 }
-
构造方法
在java中,创建一个类的对象时,会自动调用该类的构造方法,构造方法分为
默认构造方法
和自定义构造方法
,构造方法是一种特殊的实例方法,具有特殊功能作用:成员变量的初始化
构造方法名称与类名相同,没有返回值,并且在使用new关键字创建对象时会被自动调用
java对没有写构造方法的类,会添加一个无参构造方法,如果定义了一个或多个构造方法,则将自动屏蔽掉默认的构造方法
class Demo{ int a ; //无参构造 public Demo(){ } //有参构造 public Demo(int b){ this.a = b; } }
无参与有参构造构成重载
静态初始化和实例初始化以及构造方法的执行顺序
- 首先将类加载到内存中只执行一次,然后执行静态初始化
- 执行实例初始化,因为实例初始化优先于构造方法
- 然后执行构造方法
- 注意:每次创建对象都会执行实例初始化
遮蔽
class Demo{
int a;
//有参构造
public Demo(int a){//会产生遮蔽 ,局部变量a遮挡了成员变量
a = a;
}
}
shadowing 指的是在一个作用域中使用了一个和外层作用域相同的变量名,导致内部变量"遮蔽"了外部变量,即无法直接访问外部变量
局部变量和成员变量名相同时,优先使用局部变量
解决方法:
- 如果时类变量可以使用
类名.变量名
- 如果是实例变量,可以使用
this
关键字 this表示当前对象,谁在调用这个方法,此时this就是谁
this关键字
在java中,this关键字代表当前对象,通常用于区分成员变量和方法中的局部变量
作用如下:
-
引用当前对象的成员变量
public class Demo { private int a; public void setA(int a){ this.a = a; } }
-
调用当前对象的成员方法
public class Demo { private int a; public void setA(int a){ this.a = a; } /** * 调用当前对象的成员方法 */ public void show(){ this.print(); } public void print(){ System.out.println(a); } }
-
调用当前对象的构造方法
/** * 使用this调用当前对象的构造方法 */ public Demo(){ this(100); } public Demo(int a){ this.setA(a); }
- 使用this可以解决遮蔽问题
- 不推荐使用this去调用静态成员
- this关键字只能在类的实例方法、实例初始化和构造方法中使用,不能在被static修饰的方法中使用,因为被static修饰的方法,会在类加载时被创建,this指当前对象,但是此时可能还没有创建对象,所以不能在类方法中使用this关键字,
- 对“this()”的调用必须是构造函数体中的第一条语句
包
作用:
- 存放类
- 防止命名冲突,Java中只有在不同包中的类才能重名
- 包允许在更广的范围内保护类、数据、和方法。因为根据访问规则,包外的代码有可能不能访问该类
- 包名通常是域名倒置
- java.lang包不需要导包,同包下不需要导包
- 包不能以java/javax开头
访问修饰符
访问修饰符有三个四种:
public
:被public修饰的成员变量和成员方法可以在所有类中访问protected
:修饰的成员变量和成员方法可以在同包以及不同包的子类中访问package-private
(包访问修饰符):不写修饰符,可以在同包中访问private
:修饰的成员变量和成员方法只能当前类中访问
类名只能被public修饰或者不写,但是不写,那么在不同包中便无法访问,一个java源文件中只能有一个被public修饰的类
封装
-
什么是封装
java中封装的实质就是将类的状态信息(成员变量)、方法等隐藏在类的内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息(成员变量)的操作和访问。在java中可以使用访问修饰符来控制类中的数据和方法的访问级别,从而实现封装。
-
为什么要封装
封装可以提高代码的可维护性和可扩展性,并且可以保护类的数据安全性,使得代码更加健壮。
-
封装的优点
- 防止外部直接访问类的内部数据,可以保护数据的安全性
- 通过限制外部访问,可以更好的控制数据的正确性和完整性
- 可以隐藏类的实现细节,使得类的用户不需要了解类的内部实现细节
-
如何封装
1.将类中的字段改为private修饰
2.设置getter/setter方法
-
Getter方法 get加属性名(首字母大写)
返回类型:通常与属性的类型相同
方法体:直接返回属性的值
-
Setter方法 set加属性名(首字母大写)
参数:通常只有一个,参数类型与属性的类型相同
方法体:将传入的参数值赋给属性
package com.kfm.day0816; /** * @author by FZB * @date 2023/8/16 */ public class Student { private String name; private int age; //无参构造 public Student(){ } //有参构造 public Student(String name,int age){ this.name = name; this.age = age; } public void setName(String name){ this.name = name; } public String getName(){ return name; } public void setAge(int age){ this.age = age; } public int getAge(){ return age; } }
-
包装类
-
介绍: 在java中基本数据类型与对象类型是两个不同的概念,为了使基本数据类型也具备面向对象的特性,java提供了包装类
包装类是一种特殊的类,用于将基本数据类型封装成对象,java中提供了八种包装类,分别对应8种基本数据类型
基本数据类型 包装类 boolean Boolean byte Byte short Short int Integer long Long float Float double Double char Character -
装箱和拆箱
java中拆箱和装箱是指基本数据类型和对应的包装类之间的转换
- 装箱: 将
基本数据类型转换为对应的包装类对象
,可以使用包装类的构造方法或静态方法valueOf()来完成. - 拆箱: 将
包装类对象转换为对应的基本数据类型,
可以使用包装类提高的xxxValue()方法来完成
- 装箱: 将
-
自动装箱/自动拆箱(jdk5新特性)
当基础类型与它们的包装类有如下几种情况时,编译器会自动帮我们进行自动装箱或拆箱:
- 进行赋值操作
=
(装箱或拆箱) - 进行
+ - * /
混合运算(拆箱) - 使用
< > ==
比较运算(拆箱) - 调用
equals()
进行比较
- 进行赋值操作
基本数据类型和引用数类型的区别
基本数据类型是存储数据的简单类型,而引用数据类型是存储对象的引用或地址。
基本数据类型在内存中分配固定的空间,而引用数据类型在内存中分配一个地址,实际数据存储在另外的位置。
基本数据类型是直接存储在栈(stack)中的,而引用数据类型在栈中存储的是一个地址,这个地址指向堆(heap)中的实际数据。
基本数据类型有8种:byte、short、int、long、float、double、char、boolean,而引用数据类型有类(class)、接口(interface)、数组(array)、枚举(enum)等。
基本数据类型是直接存储值,而引用数据类型是存储指向对象的引用。
基本数据类型的默认值是0或false,而引用数据类型的默认值是null。
基本数据类型的传递是按值传递,而引用数据类型的传递是按引用传递。换句话说,基本数据类型的值在传递时是复制的,而引用数据类型的值在传递时是引用的副本。
继承
-
什么是继承?
继承描述的是事物之间的所属关系,这种关系是:
is-a
的关系。例如,兔子属于食草动物,食草动物属于动物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。继承:就是子类继承父类的属性和行为,使得子类对象可以直接具有与父类相同的属性、相同的行为。子类可以直接访问父类中的非私有的属性和行为。
-
为什么要继承?
继承是实现代码重用的重要手段之一,可以解决编程中代码冗余的问题,使类与类之间产生了关系。
-
怎么样继承?
//父类 public class Person{ //代码 } //子类继承父类 public class Student extends Person{ //代码 }
注意!!! java类中只支持单继承,即一个类只能由一个父类
-
继承了什么?
-
子类可以继承父类的属性和方法,子类可以使用父类的属性和方法,无需重新编写相同的代码
值得注意的是子类可以继承父类的私有成员(成员变量,方法),只是子类无法直接访问而已,可以通过getter/setter方法访问父类的private成员变量。
-
子类可以添加自己的属性和方法,子类可以增加父类中没有的属性和方法,从而添加代码的灵活性和可扩展性
-
子类不能继承父类的构造方法.
-
子类的构造方法可以调用父类的构造方法.在子类的构造方法中使用super关键字可以调用父类的构造方法,从而初始化父类的属性,如果父类没有写构造方法,那么在创建子类对象时会默认在子类构造方法第一行去隐式调用父类构造方法
-
一个类可以有多个子类,但是只能有一个父类
-
Object类是所有类的基类,每个类都是Object的子类,因此可以使用Object类中定义的一些通用方法
注意:
子父类中出现了同名的成员变量时,子类会优先访问自己对象中的成员变量。如果此时想访问父类成员变量如何解决呢?我们可以使用super关键字。
如果子类父类中出现不重名的成员方法,这时的调用是没有影响的。对象调用方法时,会先在子类中查找有没有对应的方法,若子类中存在就会执行子类中的方法,若子类中不存在就会执行父类中相应的方法。
如果子类父类中出现重名的成员方法,则创建子类对象调用该方法的时候,子类对象会优先调用自己的方法
-
-
直接继承/间接继承
类之间可以多层继承 A 继承 B , B 继承 C
-
方法重写:
如果从父类继承的方法不满足子类的需求,可以在子类中对父类同名方法进行重写(覆盖),以符合子类的需求
方法重写必须满足以下需求:
- 在继承关系中
- 重写方法与被重写方法必须有相同的方法名称
- 重写方法与被重写方法必须有相同的参数列表
- 重写方法的返回值类型必须和被重写方法的返回值类型相同或是子类
- 重写方法不能缩小被重写方法的访问权限
- 不能用子类的非静态方法重写父类的静态方法,否则编译报错
- 不能重写父类中的最终方法,被fianl修饰的方法
- 不能用子类的静态方法重写父类中的实例方法
- 静态方法可以被继承,不能被重写
@Override注解:
1、可以当注释用,方便阅读;
2、编译器可以给你验证@Override下面的方法名是否是你父类中所有的,如果没有则报错。例如,你如果没写@Override,而你下面的方法名又写错了,这时你的编译器是可以编译通过的,因为编译器以为这个方法是你的子类中自己增加的方法。 -
instanceof的作用
instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建对象时,返回true;否则,返回false。
-
final
关键字final用于修饰变量,方法和类,表示它们是不可改变的或不可继承的.
-
final修饰的类不能被继承,但是final修饰的方法可以被继承
-
final修饰的变量:final修饰的变量称为常量,一旦被初始化,就无法被修改.常量必须在声明时或在构造方法中初始化,常量的命名一般采用大写字母和下划线
-
final修饰的方法称为不可覆盖方法,子类无法重写该方法,但是可以被继承,可以被重载
-
局部变量和形参也可以使用final修饰
-
如果时引用数据类型,不能修改变量存储的地址值,但是可以修改属性值
-
-
实例化的过程
先有父类再有子类
A extends B
加载类A时,要先看类B是否被加载了(类B是否是第一次使用,如果是第一次使用,则先会将B加载到内存中),然后会进行静态初始化(static变量、static初始化器),此时类A被加载到内存中,然后进行静态初始化,类只有在第一次使用时才会进行静态初始化。
子类是父类的拓展类
静态初始化是在类被加载时执行的,而实例初始化是在创建对象时才去执行,实例初始化优先于构造方法
-
类中哪些可以继承,哪些不能继承?
- 可以继承可见的成员
- 不能继承被private修饰的成员/构造方法/static初始化器/实例初始化器
-
父类空间优先于子类对象产生:在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含其父类的成员,如果父类成员非private修饰,则子类可以随意使用父类成员。
代码体现在子类的构造调用时,一定先调用父类的构造方法。
java中的内存分配
栈
:方法运行时使用的内存,比如main方法运行,进入方法栈中执行堆
:存储对象或者数组,new来创建的,都存储在堆内存中方法区
:存储可以运行的class文件本地方法栈
:JVM在使用操作系统功能时使用,和我们开发无关寄存器
:给cpu使用,和我们开发无关
- 只要是new出来的一定是在堆空间里开辟了一个小空间
- 如果new了多次,那么在堆里面有多个小空间,每个小空间都有各自的数据
从jdk8开始,取消方法区,新增元空间,把原来方法区的多种功能进行拆分,有的功能放到了堆中,有的放到了元空间中
创建一个对象时:
- 加载class文件
- 声明局部变量
- 在堆内存中开辟一个空间
- 默认初始化
- 显示初始化
- 构造方法初始化
- 将堆内存中的地址赋值给左边的局部变量
package com.kfm.day0816;
/**
* @author by FZB
* @date 2023/8/17
*/
public class Person {
String name;
int age;
public void eat(){
System.out.println("吃饭");
}
}
package com.kfm.day0816;
/**
* @author by FZB
* @date 2023/8/17
*/
public class PersonDemo {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.name+person.age);
person.name = "张三";
person.age = 18;
System.out.println(person.name+person.age);
person.eat();
}
}
方法隐藏
父类和子类拥有相同名字的属性或者方法,方法隐藏只有一种形式,就是父类和子类存在签名相同的静态方法时。
隐藏是对于静态方法和成员变量而言的
-
当发生隐藏时,声明类型是什么类就调用对应类的方法。
-
属性只能被隐藏不能被覆盖
-
不能使用子类的静态方法隐藏父类中的非静态方法,否则编译报错
-
变量可以交叉隐藏(与static无关):
1.instance-field hiding instance-field 子类实例变量隐藏父类实例变量
2.instance-field hiding static-field 子类实例变量隐藏父类静态变量
3.static-field hiding instance-field 子类静态变量隐藏父类实例变量
4.static-field hiding static-field 子类静态变量隐藏父类静态变量
此时如果要使用父类被隐藏的变量那么就要使用super关键字
-
隐藏和重写的区别:
1.隐藏的是类的方法,使用static关键字修饰的方法
2.重写是实例方法,没有使用static关键字修饰的方法
子类 可以继承 父类中的静态方法,但是 不能重写 父类中的静态方法。
super关键字
super关键字和this关键字的作用类似,都是将被隐藏的成员变量、成员方法变得可见
super关键字不仅可以访问父类的构造方法,还可以访问父类的成员,包括父类的字段、普通方法等。
格式:
调用父类的构造方法:super([实参列表])
调用父类的属性和方法:super.<父类字段名/方法名>
注意:
- super只能出现在子类(子类的实例方法或构造方法)中,而不是其他位置
- super用来访问父类成员,如父类的属性、方法、构造方法等
- 具有访问权限的控制,无法通过super访问父类的私有成员
- super用在子类构造函数中时,必须是子类构造函数的第一行
- 子类中重载构造函数书,this()和super()只能出现一个,不能同时出现(super() 和 this() 都必须是在构造方法的第一行,所以不能同时出现。)
创建子类对象时需要先创建父类对象,默认在构造方法第一行调用父类的无参构造创建. 调用代码可以省略
如果父类没有无参构造,则需要在每一个构造方法的第一行通过 super 关键字显式去调用父类的构造方法。
对 super()的调用必须是构造函数体中的第一条语句,在成员方法中不能调用 super( ),在构造方法中调用时必须在第一行出现,默认在构造方法的第一行调用父类的构造方法,调用代码是可以省略的
this和super的区别
-
this代表当前对象,super代表父类对象
this.成员变量 -- 本类的 super.成员变量 -- 父类的 this.成员方法名() -- 本类的 super.成员方法名() -- 父类的 super(...) -- 调用父类的构造方法,根据参数匹配确认 this(...) -- 调用本类的其他构造方法,根据参数匹配确认
-
调用方法时
- this访问本类中的方法,包括从父类继承的方法
- super访问父类的方法
-
调用构造方法
- this([参数列表])表示调用本类的构造方法,只能在构造方法中调用,只能放在构造方法的首行
- super([参数列表])表示调用父类的构造,必须放在子类构造首行。如果表示调用父类的无参构造,即super()可以省略
-
调用字段
- this.字段 调用当前对象的字段,包括从父类继承的字段
- super.字段 调用的是父类中的字段
-
使用
- this在实例方法中可以使用,在static方法中不能使用,因为this表示当前对象,而使用static修饰的方法,与静态成员变量一样,属于类本身,在类装载的时候被装载到内存中,不自动进行销毁,会一直存在内存中,可以直接通过类名.方法名进行调用,而此时有可能还没有实例化对象
- super在实例方法中可以使用,在static方法中不能使用,理由同上
实例化的顺序
如果考虑到父类,其初始化顺序为:父类静态变量初始零值 -> 父类静态变量显式赋值 -> 父类静态代码块赋值 -> 子类静态变量初始零值 -> 子类静态变量显式赋值 -> 子类静态代码块赋值 -> 实例变量默认零值 -> 父类构造代码块赋值(父类实例显式赋值)统称实例初始化 -> 父类构造函数赋值 -> 子类构造代码块赋值(子类实例显式赋值)统称实例初始化 -> 子类构造函数赋值
完成类加载后,new指令就会在堆中分配一块内存空间给实例对象,虚拟机会将实例字段都初始化为零值,这一步操作保证了对象的实例字段在java代码中可以不赋初值就可以直接访问,程序能访问到这些字段的数据类型所对应的零值。这里的实例字段也包括从父类中继承下来的字段。
例子
class Parent {
private int p1 = 100; //实例变量显式初始化
private static int p2 = 10; //静态变量显式初始化
{
p1 = 101; //构造代码块初始化
}
static {
p2 = 11; //静态代码块初始化
}
public Parent() { //构造函数初始化
this.p1 = 102;
this.p2 = 12;
}
}
public class Son extends Parent{
private int s1 = 100;
private static int s2 = 10;
{
s1 = 101;
}
static {
s2 = 11;
}
public Son() {
this.s1 = 102;
this.s2 = 12;
}
public static void main(String[] args) {
Son son = new Son();
}
}
上述代码赋值流程:
- p2 = 0 (父类加载的准备阶段,静态变量设置默认零值)
- p2 = 10 , p2 = 11(父类加载的初始化阶段,执行clinit)
- s2 = 0 (子类加载的准备阶段,静态变量设置默认零值)
- s2 = 10 , s2 = 11(子类加载的初始化阶段,执行clinit)
- p1 = 0,s1 = 0 (子类对象在堆空间分配,实例字段设置默认零值,这里实例字段也包括从父类中继承来的字段)
- p1 = 100, p1 = 101, p1 = 102,p2 = 12 (父类对象执行init函数,包括显式初始化,构造代码块和构造函数)
- s1 = 100, s1 = 101, s1 = 102,s2 = 12 (子类对象执行init函数,包括显式初始化,构造代码块和构造函数)
父类
package com.kfm.day0817;
/**
* @author by FZB
* @date 2023/8/17
静态初始化和显式初始化哪个先执行
*/
public class Sup {
int a = 1;
static {
b = 3;
}
static int b = 2;
public Sup(int a,int b){
print();
this.a = a;
this.b = b;
print();
}
public void print(){
System.out.println(a);
System.out.println(b);
}
}
子类
package com.kfm.day0817;
/**
* @author by FZB
* @date 2023/8/17
*/
public class Sub extends Sup{
int a = 11;
int b = 22;
public Sub(int a, int b) {
super(a, b);
print();
}
@Override
public void print() {
System.out.println(a);
System.out.println(b);
}
public static void main(String[] args) {
Sub sub = new Sub(6,9);
sub.print();
}
}
执行顺序:
1.首先加载父类,父类静态变量初始化零值,此时b = 0,然后执行静态初始化此时b = 3 静态变量显式赋值 此时b = 2
2.然后加载子类,执行main方法,mian方法入栈
3.创建Sub对象时,使用new关键字,此时在堆内存中分配了一块空间给sub对象,然后默认初始化a = 0, b = 0
4.然后在子类构造方法第一行去执行父类构造方法,此时父类构造方法中的print其实是调用的子类中的方法,此时子类中还没有进行显式初始化,所以打印出来a =0 b = 0
5.直到父类构造方法执行完毕之后,才去执行子类中的实例初始化,此时a = 11 , b = 12 ,然后再去执行子类构造方法,此时print()方法打印出来的a = 11 , b =22
6.最后使用对象名调用print ()方法,此时打印出来的是a = 11 , b = 22
所以最终结果为 0 0 0 0 11 22 11 22
注意:
调用static方法时,看对象的类型
调用实例方法时,看对象是谁
多态
-
什么是多态?
多态就是同一种行为,不同形式的表现(比如动物叫声,狗叫和猫叫是同一种行为,但是是不同的形式)
-
多态体现的形式
父类类型 变量名 = new 子类/实现类构造器; 变量名.方法名();
-
实现多态的前提
1.是继承关系
2.重写是继承的前提
3.父类引用指向子类对象(父类引用可以指向任意一个子类的对象)
-
父类引用调用方法时,实际上是调用的子类的方法,不同的子类有不同的方法实现,体现出同一个方法在不同子类中的不同形态的表现
-
调用实例方法时看等号的右边,调用静态方法看等号的左边(意思是如果使用父类引用调用实例方法时,执行的时new 的对象的方法,调用静态方法时,调用的是父类中的静态方法)
-
重写是运行时多态,重载时编译时多态
-
调用成员变量时:编译看左边,运行看左边
调用成员方法时:编译看左边,运行看右边
Fu f = new Zi(); //编译看左边的父类中有没有name这个属性,没有就报错 //在实际运行的时候,把父类name属性的值打印出来 System.out.println(f.name); //编译看左边的父类中有没有show这个方法,没有就报错 //在实际运行的时候,运行的是子类中的show方法 f.show();
-
-
父类引用只能调用中父类中声明的方法,不能调用子类中的拓展方法
-
为什么要转型
多态的写法就无法访问子类独有功能。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
回顾基本数据类型转换
- 自动转换: 范围小的赋值给范围大的.自动完成:double d = 5;
- 强制转换: 范围大的赋值给范围小的,强制转换:int i = (int)3.14
多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。
-
向上转型(自动转换)
子类向父类转换称为向上转型,当父类引用指向一个子类对象时,便是向上转型。
格式: 父类类型 引用变量名 = new 子类类型();
**原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。**所以子类范围小可以直接自动转型给父类类型的变量。
-
向下转型(自动转换)
当发生向上转型后,无法调用子类新增的方法,但是如果需要调用子类新增的方法可以通过把父类转换为子类实现
格式: 子类类型 引用变量名 = (子类类型)父类类型的引用变量
-
instanceof关键字
在向下转型过程中,如果不是存在为真实的子类类型,会出现类型转换异常,所以在java中提供了instanceof运算符来进行类型转换的判断
例子:
Pet pet = new Dog();//父类引用pet指向子类对象Dog pet.eat();//调用狗的吃饭方法 Cat car = (Cat)pet;//此时会出现类型转换异常
变量名 instanceof 数据类型 如果变量属于该数据类型或者其子类类型,返回true。 如果变量不属于该数据类型或者其子类类型,返回false。
使用instanceof时,对象的类型必须和instanceof后面的参数所指定的类有继承关系,否则会出现编译错误
在jdk16的增强以后,对于instanceof的判断以及类型转换可以合二为一了
例子: if(pet instanceof Dog dog){ // }
-
多态的应用:
1.使用父类作为方法的参数
2.使用父类作为方法的返回值
- 当一个方法的形参是一个类,我们可以传递这个类所有的子类对象。
- 当一个方法的形参是一个接口,我们可以传递这个接口所有的实现类对象(后面会学)。
- 而且多态还可以根据传递的不同对象来调用不同类中的方法
-
里氏替换
在Java中,里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计的基本原则之一。这个原则是由美国计算机科学家Barbara Liskov提出的。
里氏替换原则的基本思想是,如果一个程序使用了一个基类的函数,那么在任何使用基类的地方,都可以使用其子类而不会引入任何错误。也就是说,如果一个子类继承了基类,那么子类应该能够替换基类,而不会破坏程序的任何功能。
这个原则的实践意义在于,它确保了子类可以在不改变程序行为的前提下扩展基类的功能。同时,如果一个子类不能符合基类的行为,那么它就不是一个真正的子类,也就违反了里氏替换原则。
举个例子来说明:
假设我们有一个基类
Base
:public class Base { public void setValue(int value) { this.value = value; } public int getValue() { return value; } private int value; }
现在我们创建一个子类
Derived
:public class Derived extends Base { @Override public void setValue(int value) { if (value >= 0) { super.setValue(value); } else { throw new IllegalArgumentException("Value must be non-negative"); } } }
在这个例子中,
Derived
类扩展了Base
类,并重写了setValue
方法以添加非负检查。这意味着,在任何使用Base
类的地方,我们都可以使用Derived
类,而不会破坏任何功能。这就是里氏替换原则的一个例子。 -
多态的优势:
1.可替换性:多态已存在的代码具有可替换性。
2.可扩充性:多态代码具有可扩充性。增加的子类不影响已存在的类的多态性、继承性,以及其他特性的运行和操作。
3.接口性:多态是父类向子类提供了一个共同接口,由子类来具体实现。
4.灵活性
5.简化性:多态简化了应用软件的代码编写和修改过程,尤其在处理大量对象的运算符和操作时,这个特点尤为突出。
Object类
Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。
Object 类可以显式继承,也可以隐式继承。
Object类中常用的方法:
-
String toString():返回当前对象本身的有关信息,返回字符串对象,可以重写toString方法
Object中的toString方法
返回对象的字符串表示形式。返回:对象的字符串表示形式。API注意:通常,toString方法返回一个字符串,该字符串“以文本方式表示”此对象。结果应该是一个简洁但信息丰富的表述,易于阅读。建议所有子类重写此方法。字符串输出不一定随时间或JVM调用而稳定。实现要求:Object类的toString方法返回一个字符串,该字符串由对象作为实例的类的名称、at符号字符“@”和对象哈希代码的无符号十六进制表示组成。换句话说,此方法返回一个等于以下值的字符串:getClass().getName()+'@'+Integer.toHexString(hashCode()) public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
-
boolean equals(Object):比较两个对象是否是同一个对象,是返回true,否则返回false
Object中的equals方法
指示其他对象是否与此对象“相等”。equals方法在非null对象引用上实现了一个等价关系:它是自反的:对于任何非null引用值x,x.equals(x)都应该返回true。它是对称的:对于任何非null引用值x和y,x.equals(y)应返回true,当且仅当y.equals(x)返回true。它是可传递的:对于任何非空的引用值x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)应该返回true。它是一致的:对于任何非null引用值x和y,如果不修改对象上的equals比较中使用的信息,则多次调用x.equals(y)会一致地返回true或一致地返回false。对于任何非null引用值x,x.equals(null)应返回false。等价关系将其操作的元素划分为等价类;等价类的所有成员彼此相等。对等成员 //比较两个对象地址值是否相同 public boolean equals(Object obj) { return (this == obj); }
例子:String类中重写了equals对象
equals() 方法用于将字符串与指定的对象比较。
String 类中重写了 equals() 方法用于比较两个字符串的内容是否相等
public boolean equals(Object anObject) { if (this == anObject) { return true; } return (anObject instanceof String aString) && (!COMPACT_STRINGS || this.coder == aString.coder) && StringLatin1.equals(value, aString.value); }
重写 equals 要求:
- 对 null 返回 false
- 自反性:x.equals(x) 返回true
- 对称性: x.equals(y) 的值 和 y.equals(x) 的值一样
- 传递性: x.equals(y) 为 true, y.equals(z) 为 true, 此时 x.equals(z) 为 true
- 一致性: x.equals(y) 多次调用结果一致
- 重写了 equals 就要重写 hashCode
两个对象equals()结果为true,hashCode相等
两个对象hashCode相等,equlas结果不一定为true
在调用equals()方法时,调用对象不能为null,否则会报空指针异常
例子
@Override public boolean equals(Object obj) { if (obj == null) { return false; } // 谁调用equals方法,谁就是this if (this == obj) { return true; } // instanceof 其类型的对象或其子类型的对象都是 true // 同一个类。如果值允许说 同一个类的对象才能判断true 则使用 getClass if(this.getClass() == obj.getClass()) if (obj instanceof Student stu){ // 同一个类型。只要是 student 或者 student的子类都可以 // 大多数情况下,对象的每个属性都一样则认为 equals 为 true if (!stu.id.equals(this.id)) { return false; } if (!stu.name.equals(this.name)){ return false; } if (!stu.age.equals(this.age)){ return false; } if (!stu.gender.equals(this.gender)){ return false; } return true; } return false; }
-
Object clone():生成当前对象的一个副本,并返回
clone 方法是浅拷贝,对象内属性引用的对象只会拷贝引用地址,而不会将引用的对象重新分配内存,相对应的深拷贝则会连引用的对象也重新创建。
由于 Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
-
int hashCode():返回该对象的哈希码值
哈希表(Hash表)是一种根据键(Key)而直接访问在内存存储位置的数据结构。它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数叫做哈希函数(Hash函数),存放记录的数组叫做哈希表(Hash表)。
hashCode()的作用是获取哈希码,也称散列码,它实际是返回一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置,hashCode()定义在jdk的Object.java中,是一个本地方法,使用native修饰,
如果没有重写就将内存地址转为int类型进行返回
,哈希码的作用就是确定对象在哈希表中的索引位置 将java中的任何类都包含有hashCode函数散列表存储的是键值对,特点是能根据"键"快速的检索出相应的值,这其中就利用到了散列码
-
为什么要有hashCode?
如果没有哈希码我们在存储对象时每次都得从头开始遍历,按个和每个对象进行equal比较,但是效率太低了
在存放对象是可以通过一定的计算规则来获取要存放的索引位置,但是哈希码是可能会重复的,因为哈希码只是通过一定的逻辑计算出来的int数值,两个不同的对象,哈希码完全有可能会相同,这就是哈希冲突,当要存储的对象和已经存储的对象发生哈希冲突时,可以先去判断两个对象是否相等,如果不相等再使用别的办法存起来,两个哈希冲突的对象,使用equals来判断是否相等
如果只重写了hashCode方法,当哈希冲突时即使两个对象相等,也不会判定为重复,会导致存储重复对象
如果只重写了equals方法,那么两个相等的对象,内存地址不会相等,还是会造成重复添加元素
以HashSet如何检查重复性为例子数说明
对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有HashSet会假设该对象没有重复出现,如果发现有值(哈希冲突),就会去调用equals方法来检查两个对象是否真的相同,如果两者相同,HashSet就不会让其加入操作成功,如果不同的话,就会重新散列到其他位置,这样大大减少了equals方法,提高了执行速度
重写hashCode:
/* equals 结果为 true hashCode 值一样 hashCode 一样 equals 结果不一定 */ @Override public int hashCode() { int result = 1; result = 31 * result + name.hashCode(); result = 31 * result + age; result = 31 * result + gender; result = 31 * result + id.hashCode(); return result; }
-
-
Class getClass()方法:获取类结构信息,返回Class对象
Object getClass() 方法用于获取对象的运行时对象的类。
按个和每个对象进行equal比较,但是效率太低了
在存放对象是可以通过一定的计算规则来获取要存放的索引位置,但是哈希码是可能会重复的,因为哈希码只是通过一定的逻辑计算出来的int数值,两个不同的对象,哈希码完全有可能会相同,这就是哈希冲突,当要存储的对象和已经存储的对象发生哈希冲突时,可以先去判断两个对象是否相等,如果不相等再使用别的办法存起来,两个哈希冲突的对象,使用equals来判断是否相等
如果只重写了hashCode方法,当哈希冲突时即使两个对象相等,也不会判定为重复,会导致存储重复对象
如果只重写了equals方法,那么两个相等的对象,内存地址不会相等,还是会造成重复添加元素
以HashSet如何检查重复性为例子数说明
对象加入HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,看该位置是否有值,如果没有HashSet会假设该对象没有重复出现,如果发现有值(哈希冲突),就会去调用equals方法来检查两个对象是否真的相同,如果两者相同,HashSet就不会让其加入操作成功,如果不同的话,就会重新散列到其他位置,这样大大减少了equals方法,提高了执行速度
重写hashCode:
/*
equals 结果为 true hashCode 值一样
hashCode 一样 equals 结果不一定
*/
@Override
public int hashCode() {
int result = 1;
result = 31 * result + name.hashCode();
result = 31 * result + age;
result = 31 * result + gender;
result = 31 * result + id.hashCode();
return result;
}
-
Class getClass()方法:获取类结构信息,返回Class对象
Object getClass() 方法用于获取对象的运行时对象的类。