文章目录
面向过程的程序设计思想(Process-Oriented Programming),简称 POP
关注的焦点是 过程 :过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个 函数 。这样就可以大大简化冗余代码,便于维护。
典型的语言:C语言
代码结构:以 函数 为组织单位。
是一种“ 执行者思维 ”,适合解决简单问题。扩展能力差、后期维护难度较大
面向对象的程序设计思想( Object Oriented Programming),简称 OOP
关注的焦点是 类 :在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽
象出来,用类来表示。
典型的语言:Java、C#、C++、Python、Ruby和PHP等
代码结构:以 类 为组织单位。每种事物都具备自己的 属性 和 行为/功能 。
是一种“ 设计者思维 ”,适合解决复杂问题。代码扩展性强、可维护性高。
Java语言的基本元素:类和对象
类:具有相同特征的事物的抽象描述,是 抽象的 、概念上的定义
对象:实际存在的该类事物的 每个个体 ,是 具体的 ,因而也称为 实例(instance) 。
面向对象程序设计的重点是 类的设计 类的设计,其实就是 类的成员的设计
Java中用类class来描述事物也是如此。类,是一组相关 属性 和 行为 的集合,这也是类最基本的两个成员。
属性:该类事物的状态信息。对应类中的 成员变量
成员变量 <=> 属性 <=> Field
行为:该类事物要做什么操作,或者基于事物的状态能做什么。对应类中的 成员方法
(成员)方法 <=> 函数 <=> Method
类的定义
[修饰符] class 类名{
属性声明;
方法声明;
}
public class Person{
//声明属性age
int age ;
//声明方法showAge()
public void eat() {
System.out.println("人吃饭");
}
}
对象的创建
创建对象,使用关键字:new
# 方式1:给创建的对象命名
# 把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了
类名 对象名 = new 类名();
# 方式2:
new 类名()//也称为匿名对象
class PersonTest{
public static void main(String[] args){
//创建Person类的对象
Person per = new Person();
//创建Dog类的对象
Dog dog = new Dog();
}
}
对象调用属性或方法
对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。
使用" 对象名.属性 " 或 " 对象名.方法 "的方式访问对象成员(包括属性和方法)
//声明Animal类
public class Animal { //动物类
public int legs;
public void eat() {
System.out.println("Eating.");
}
public void move() {
System.out.println("Move.");
}
}
//声明测试类
public class AnimalTest {
public static void main(String args[]) {
//创建对象
Animal xb = new Animal();
xb.legs = 4;//访问属性
System.out.println(xb.legs);
xb.eat();//访问方法
xb.move();//访问方法
}
}
类的实例化(创建类的对象)
public class Game{
public static void main(String[] args){
Person p = new Person();
//通过Person对象调用属性
p.name = "康师傅";
p.gender = '男';
p.dog = new Dog(); //给Person对象的dog属性赋值
//给Person对象的dog属性的type、nickname属性赋值
p.dog.type = "柯基犬";
p.dog.nickName = "小白";
//通过Person对象调用方法
p.feed();
}
}
匿名对象(anonymous object)
我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。如:new Person().shout();
使用情况 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象。我们经常将匿名对象作为实参传递给一个方法调用
匿名对象只能用一次 后面就不能调用
对象的内存解析
JVM内存结构划分 HotSpot Java虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分
堆(Heap) :此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。
栈(Stack) :是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference)类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。
方法区(Method Area) :用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
堆:凡是new出来的结构(对象、数组)都放在堆空间中。
对象的属性存放在堆空间中。
创建一个类的多个对象(比如p1、p2),则每个对象都拥有当前类的一套"副本"(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。
当声明一个新的变量使用现有的对象进行赋值时(比如p3 = p1),此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用
对象名中存储的是什么呢? 对象地址
直接打印对象名和数组名都是显示“类型@对象的hashCode值",所以说类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。
关键字:this
this 它在方法(准确的说是实例方法或非static的方法)内部使用, 当前调用构造器的对象
表示调用该方法的对象它在构造器内部使用,表示该构造器正在初始化的对象。
this可以调用的结构:成员变量、方法和构造器
public class PersonTest {
public static void main(String[] args) {
Person person = new Person();
person.setAge(10);
System.out.println(person.getAge());
}
}
class Person{
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
什么时候使用this?
A1实例方法或构造器中使用当前对象的成员
在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的可读性。不过,通常我们都习惯省略this。但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量
和局部变量
。
public class Student{
String name;
public void setName(String name){
this.name = name;
}
}
A2同一个类中构造器互相调用
this可以作为一个类中构造器相互调用的特殊格式。 this调用构造器时必须放在首行 只能调1个
- this():调用本类的无参构造器
- this(实参列表):调用本类的有参构造器
public class Student {
private String name;
private int age;
// 无参构造
public Student() {
// this("",18);//调用本类有参构造器
}
// 有参构造
public Student(String name) {
this();//调用本类无参构造器
this.name = name;
}
// 有参构造
public Student(String name,int age){
this(name);//调用本类中有一个String参数的构造器
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}
面向对象特征二:继承(Inheritance)
父类 superClass
子类 subClass is-a
默认父类 Object
从上而下
class Person{
public String name;
public int age;
public Date birthDate;
public String getInfo(){
//...
}
}
class Student extends Person {
public String school;
}
/*Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。*/
从下而上
class Animal{
public String name;
public int age;
public void eat(){
//....
}
}
class Cat{
public String name;
public int age;
public void eat(){
//....
}
}
class Dog{
public String name;
public int age;
public void eat(){
//....
}
}
/*多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系*/
- 继承的出现减少了代码冗余,提高了代码的复用性。
- 继承的出现,更有利于功能的扩展。
- 继承的出现让类与类之间产生了
is-a
的关系,为多态的使用提供了前提。 - 继承描述事物之间的所属关系,这种关系是:
is-a
的关系。可见,父类更通用、更一般,子类更具体
注意:不要仅为了获取其他类中某个功能而去继承! 子类继承父类需要父类有无参构造器
//继承的语法
[修饰符] class 类A {
...
}
[修饰符] class 类B extends 类A {
...
}
//继承中的基本概念
//类B,称为子类、派生类(derived class)、SubClass
//类A,称为父类、超类、基类(base class)、SuperClass
1 子类会继承父类所有的实例变量和实例方法
从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。
- 当子类对象被创建时,在堆中给对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存。
- 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,会看它的父类甚至父类的父类是否声明了这个方法,遵循
从下往上
找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。(自下而上)
所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。
2、子类不能直接访问父类中私有的(private)的成员变量和方法
子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,可通过继承的get/set方法进行访问。
3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”
子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展
4、Java支持多层继承(继承体系)
class A{}
class B extends A{}
class C extends B{}
//子类和父类是一种相对的概念
//顶层父类是Object类。所有的类默认继承Object,作为父类。
5、一个父类可以同时拥有多个子类
class A{}
class B extends A{}
class D extends A{}
class E extends A{}
6、Java只支持单继承,不支持多重继承
public class A{}
class B extends A{}
//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} //ok
class C extends A,B... //error
方法的重写(override/overwrite)
父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)
。也称为方法的重置
、覆盖
。
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
@Override使用说明:
写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。
方法重写的要求
-
子类重写的方法
必须
和父类被重写的方法具有相同的方法名称
、参数列表
。 -
子类重写的方法的返回值类型
不能大于
父类被重写的方法的返回值类型。(例如:Student < Person)。注意:如果返回值类型是基本数据类型和void,那么必须是相同 父类返回值类型void 子类重写方法也必须void
-
子类重写的方法使用的访问权限
不能小于
父类被重写的方法的访问权限。(public > protected > 缺省 > private)注意:① 父类私有方法不能重写 private ② 跨包的父类缺省的方法也不能重写
-
子类方法抛出的异常不能大于父类被重写方法的异常
此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。
方法的重载与重写
方法的重载:同一类 方法名相同,形参列表不同。不看返回值类型。
方法的重写:子类可以对从父类中继承来的方法进行改造
// 同一类中
// 方法重载
public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}
// 父子类中
public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法
Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}
class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
@Override
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}
再谈封装性中的4种权限修饰
权限修饰符:public,protected,缺省,private
外部类:public和缺省
成员变量、成员方法等:public,protected,缺省,private
跨包 属性访问 extend public protected 对象访问 public
1、外部类要跨包使用必须是public,否则仅限于本包使用
(1)外部类的权限修饰符如果缺省,本包使用没问题
(2)外部类的权限修饰符如果缺省,跨包使用有问题
2、成员的权限修饰符问题
(1)本包下使用:成员的权限修饰符可以是public、protected、缺省
(2)跨包下使用:要求严格 public
(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义
调试debug第一个按钮 进入下一行 第二个按钮 进入当前行代码
关键字:super
在Java类中使用super来调用父类中的指定操作:
- super可用于访问父类中定义的属性
- super可用于调用父类中定义的成员方法
- super可用于在子类构造器中调用父类的构造器
- 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
- super的追溯不仅限于直接父类
- super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识
super的使用场景
1 子类中调用父类被重写的方法
- 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
- 如果子类重写了父类的方法,在子类中需要通过
super.
才能调用父类被重写的方法,否则默认调用的子类重写的方法
public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
//smartphone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}
-
方法前面没有super.和this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
方法前面有this.
- 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
-
方法前面有super.
- 从当前子类的直接父类找,如果没有,继续往上追溯
2 子类中调用父类中同名的成员变量
- 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
- 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
- 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;
public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找
System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}
就近原则
-
变量前面没有super.和this.
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
局部变量
, - 如果不是局部变量,先从当前执行代码的
本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找
父类声明的成员变量
(权限修饰符允许在子类中访问的)
- 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的
-
变量前面有this.
- 通过this找成员变量时,先从当前执行代码的本类去找成员变量
- 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
-
变量前面super.
- 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
- 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)
特别说明:应该避免子类声明和父类重名的成员变量
3 子类构造器中调用父类构造器
① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器。
② 规定:“super(形参列表)”,必须声明在构造器的首行。
③ 我们前面讲过,在构造器的首行可以使用"this(形参列表)",调用本类中重载的构造器,
结合②,结论:在构造器的首行,“this(形参列表)” 和 "super(形参列表)"只能二选一。
④ 如果在子类构造器的首行既没有显示调用"this(形参列表)“,也没有显式调用"super(形参列表)”,
则子类此构造器默认调用"super()",即调用父类中空参的构造器。
⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
只能是这两种情况之一。
⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了"this(形参列表)“,则剩下的那个一定使用"super(形参列表)”。
通过子类构造器创建对象时 一定在调用子类构造器过程中直接或间接调用了父类的构造器
创建子类对象后 子类对象就获取父类声明的所有属性方法 权限允许下 可以 直接调用
class A{
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}
this与super
1、this和super的意义
this:当前对象
- 在构造器和非静态代码块中,表示正在new的对象
- 在实例方法中,表示调用当前方法的对象
super:引用父类声明的成员
2、this和super的使用格式
- this
- this.成员变量:表示当前对象的某个成员变量,而不是局部变量
- this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
- this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
- super
- super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
- super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
- super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错
面向对象特征三:多态性
(左边父类引用 右边子类对象 父类引用指向子类对象) 子类对象的多态性 虚拟方法调用
对象的多态性:父类的引用指向子类的对象
// 父类类型 变量名 = 子类对象;
Person p = new Student();
Object o = new Person();//Object类型的变量o,指向Person类型的对象
o = new Student(); //Object类型的变量o,指向Student类型的对象
对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象
Java引用变量有两个类型:编译时类型
和运行时类型
。编译时类型由声明
该变量时使用的类型决定,运行时类型由实际赋给该变量的对象
决定。简称:编译时,看左边;运行时,看右边。
多态体现在事件行为上 不体现在属性上
- 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)
- 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
多态的使用前提:① 类的继承关系 ② 方法的重写
多态场景下 调用方法时 编译认为左边声明父类的类型 执行时子类重写父类的方法 虚拟方法 (适用与方法 不适用属性)
父类子类同一属性时 多态调用父类属性 属性不满足
多态应用场景 避免重复声明
1、方法内局部变量的赋值体现多态
public class AnimalTest {
public static void main(String[] args) {
AnimalTest animalTest = new AnimalTest();
//animalTest.method(new Animal());
animalTest.method(new Dog());
}
public void method(Animal animal){ // Animal animal = new Dog()
animal.eat();
}
}
class Animal{
public void eat(){
System.out.println("animal eat");
}
public void jump(){
System.out.println("animal jump");
}
}
class Dog extends Animal{
public void eat(){
System.out.println("dog eat born");
}
// 多态不能调子类独有的方法
public void watch(){
System.out.println("dog watch gate");
}
}
class Cat extends Animal{
public void eat(){
System.out.println("cat eat mouse");
}
public void jump(){
System.out.println("cat jump height");
}
}
public class TestPet {
public static void main(String[] args) {
//多态引用
Pet pet = new Dog();
pet.setNickname("小白");
//多态的表现形式
/*
编译时看父类:只能调用父类声明的方法,不能调用子类扩展的方法;
运行时,看“子类”,如果子类重写了方法,一定是执行子类重写的方法体;
*/
pet.eat();//运行时执行子类Dog重写的方法
// pet.watchHouse();//不能调用Dog子类扩展的方法
pet = new Cat();
pet.setNickname("雪球");
pet.eat();//运行时执行子类Cat重写的方法
}
}
多态性(Polymorphism)在面向对象编程中具有重要的作用,它使得代码更加灵活、可扩展和易于维护。
以下是多态性的一些主要作用:
- 代码复用:多态允许将多个不同的类对象视为同一类型,通过共同的接口或父类引用来调用方法。这样可以实现代码的复用,减少代码的冗余性。
- 扩展性:通过多态性,可以方便地添加新的子类或扩展已有的类,而不需要修改现有的代码。只需基于已有的接口或父类定义新的子类,并实现相应的方法即可。
- 简化代码: 多态性能够提高代码的可读性和可维护性。通过使用多态性,我们可以将大量的条件判断语句转换为统一的接口调用,使得代码更加简洁和易于理解。
- 解耦合: 多态性降低了类之间的耦合度。客户端代码只需要与抽象类型进行交互,而不需要依赖具体的实现类。这使得系统更加灵活和可扩展,可以方便地替换实现类或添加新的实现类。
- 运行时动态绑定: 多态性通过运行时动态绑定实现方法的调用。这意味着在运行时,具体调用哪个方法将基于实际对象的类型来决定,而不是基于编译时的类型。这提供了灵活性和适应性,允许在运行时确定正确的方法实现。
总之,多态性使得代码更加灵活和可扩展,提高了代码的复用性和可维护性,并降低了类之间的耦合度。它是面向对象编程中重要的概念,有助于编写出更优雅、可扩展和易于维护的代码。
多态符合开闭原则 对扩展开发 对修改关闭
多态弊端 创建子类对象 声明父类引用不能直接调用子类独有方法属性 需要下转类型
假设我们有一个基类 Animal
和两个子类 Cat
和 Dog
public class Animal {
public void makeSound() {
System.out.println("Some animal sound");
}
}
public class Cat extends Animal {
public void makeSound() {
System.out.println("Meow");
}
}
public class Dog extends Animal {
public void makeSound() {
System.out.println("Woof");
}
}
现在我们需要编写一个方法 makeAnimalSound()
,该方法接收一个 Animal
对象,并且调用它的 makeSound()
方法。
public class AnimalSoundMaker {
public void makeAnimalSound(Animal animal) {
animal.makeSound();
}
}
现在我们可以使用多态性在运行时决定具体调用哪个子类的方法。例如:
public class Main {
public static void main(String[] args) {
AnimalSoundMaker maker = new AnimalSoundMaker();
Animal animal1 = new Cat();
Animal animal2 = new Dog();
maker.makeAnimalSound(animal1); // 输出:Meow
maker.makeAnimalSound(animal2); // 输出:Woof
}
}
通过将 Cat
和 Dog
对象转换为 Animal
对象,并将它们传递给 makeAnimalSound()
方法,我们可以在运行时确定调用哪个子类的 makeSound()
方法。这就是多态性的作用,使代码更加灵活和可扩展。如果我们需要添加新的动物类,在不修改 AnimalSoundMaker
类的情况下,只需定义新的子类并覆盖 makeSound()
方法即可。
这种灵活性和可扩展性是多态性的主要优点之一。
如果父类的引用指向子类的对象,那么在编译时将只能调用父类中定义的方法,而无法直接调用子类扩展的方法。这是因为编译器只知道父类所具有的方法和属性。
但是,如果确实需要调用子类扩展的方法,可以使用类型转换将父类引用强制转换为子类类型。通过这种方式,就能够调用子类中新增的方法。
class Parent {
public void parentMethod() {
System.out.println("Parent Method");
}
}
class Child extends Parent {
@Override
public void parentMethod() {
System.out.println("Child Method1");
}
public void childMethod() {
System.out.println("Child Method2");
}
}
public class Main {
public static void main(String[] args) {
Parent parent = new Child(); // 父类引用指向子类对象
parent.parentMethod(); // 调用父类方法
if (parent instanceof Child) { // 检查父类引用是否可以转换为子类类型
Child child = (Child) parent; // 强制类型转换为子类类型
child.childMethod(); // 调用子类扩展的方法
}
}
}
在上面的代码中,Parent
是父类,Child
是其子类。当父类引用 parent
指向子类对象时,只能通过父类引用调用父类的方法 parentMethod()
。但是,通过使用 instanceof
运算符可以检查父类引用是否可以转换为子类类型。如果可以转换,就可以将父类引用强制转换为子类类型,并调用子类的扩展方法 childMethod()
。
需要注意的是,在进行类型转换之前,最好使用 instanceof
运算符进行类型检查,避免出现类型转换异常。
2、方法的形参声明体现多态
public class Person{
private Pet pet;
public void adopt(Pet pet) {//形参是父类类型,实参是子类对象
this.pet = pet;
}
public void feed(){
pet.eat();//pet实际引用的对象类型不同,执行的eat方法也不同
}
}
public class TestPerson {
public static void main(String[] args) {
Person person = new Person();
Dog dog = new Dog();
dog.setNickname("小白");
person.adopt(dog);//实参是dog子类对象,形参是父类Pet类型
person.feed();
Cat cat = new Cat();
cat.setNickname("雪球");
person.adopt(cat);//实参是cat子类对象,形参是父类Pet类型
person.feed();
}
}
3、方法返回值类型体现多态
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet dog = shop.sale("Dog");
dog.setNickname("小白");
dog.eat();
Pet cat = shop.sale("Cat");
cat.setNickname("雪球");
cat.eat();
}
}
多态的好处和弊端
好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定。代码编写更灵活、功能更强大,可维护性和扩展性更好了。
弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法。
Student m = new Student();
m.school = "pku"; //合法,Student类有school成员变量
Person e = new Student();
e.school = "pku"; //非法,Person类没有school成员变量
// 属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。
开发中: 使用父类做方法的形参,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。
【开闭原则OCP】
- 对扩展开放,对修改关闭
- 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能
虚方法调用(Virtual Method Invocation)
在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
动态绑定
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
成员变量没有多态性
- 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。
- 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量
public class TestVariable {
public static void main(String[] args) {
Base b = new Sub();
System.out.println(b.a);
System.out.println(((Sub)b).a);
Sub s = new Sub();
System.out.println(s.a);
System.out.println(((Base)s).a);
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}
如何向上或向下转型
向上转型:自动完成 多态 父类声明 子类new
向下转型:(子类类型)父类变量 子类声明 = (子类)父类
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog();//pet的编译时类型是Pet,运行时类型是Dog
pet.setNickname("小白");
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
System.out.println("d.nickname = " + d.getNickname());
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
instanceof关键字
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验。如下代码格式:
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
说明:
- 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
- 如果对象a属于superA的子类A,a instanceof superA值也为true。
- 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog();//多态引用
pets[0].setNickname("小白");
pets[1] = new Cat();//多态引用
pets[1].setNickname("雪球");
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat) pets[i];
cat.catchMouse();
}
}
}
}
public class Exersize {
public static void main(String[] args) {
Exersize exersize = new Exersize();
exersize.meeting(new Man(), new Woman()
);
}
public static void meeting(ExerPerson... ps) {
for (int i = 0; i < ps.length; i++) {
ps[i].eat();
ps[i].toilet();
if (ps[i] instanceof Man) {
Man man = (Man) ps[i];
man.smoke();
}
if (ps[i] instanceof Woman) {
Woman woman = (Woman) ps[i];
woman.makeup();
}
System.out.println();
}
}
}
class ExerPerson {
public void eat() {
System.out.println("吃饭");
}
public void toilet() {
System.out.println("上洗手间");
}
}
class Man extends ExerPerson {
public void eat() {
System.out.println("男人吃饭");
}
public void toilet() {
System.out.println("男人上洗手间");
}
public void smoke() {
System.out.println("抽烟");
}
}
class Woman extends ExerPerson {
public void eat() {
System.out.println("女人吃饭");
}
public void toilet() {
System.out.println("女人上洗手间");
}
public void makeup() {
System.out.println("化妆");
}
}
Object 类的使用
类 java.lang.Object
是类层次结构的根类,即所有其它类的父类。每个类都使用 Object
作为超类。
Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用
Object中没有声明属性 只有一个空参构造器
method(Object obj){…} //可以接收任何类作为其参数
Person o = new Person();
method(o);
-
所有对象(包括数组)都实现这个类的方法。
-
如果一个类没有特别指定父类,那么默认则继承自Object类。
public class Person {
...
}
//等价于:
public class Person extends Object {
...
}
Object类的方法
根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。这里我们主要关注其中的6个:
1、(重点)equals()
**equals():**所有类都继承了Object,也就获得了equals()方法。还可以重写。
-
只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象
-
格式:obj1.equals(obj2)
-
特例:当用equals()方法进行比较时,对类File、String、Date及包装类(Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对象;
- 原因:在这些类中重写了Object类的equals()方法。
-
当自定义使用equals()时,可以重写。用于比较两个对象的“内容”是否都相等
-
重写equals()方法的原则
-
对称性
:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。 -
自反性
:x.equals(x)必须返回是“true”。 -
传递性
:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。 -
一致性
:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。 -
任何情况下,x.equals(null),永远返回是“false”;
x.equals(和x不同类型的对象)永远返回是“false”。
在自定义类里重写equals方法只比较数据值相等 要同时重写自定义类
-
class User{
private String host;
private String username;
private String password;
public User(String host, String username, String password) {
super();
this.host = host;
this.username = username;
this.password = password;
}
public User() {
super();
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User [host=" + host + ", username=" + username + ", password=" + password + "]";
}
// equals方法重写
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (host == null) {
if (other.host != null)
return false;
} else if (!host.equals(other.host))
return false;
if (password == null) {
if (other.password != null)
return false;
} else if (!password.equals(other.password))
return false;
if (username == null) {
if (other.username != null)
return false;
} else if (!username.equals(other.username))
return false;
return true;
}
}
package com.java.object;
import java.util.Objects;
/**
* @author Admin
* @title: OrderEquals
* @projectName MyDemo
* @description: OrderEquals
* @date 2024/8/10 19:32
*/
public class OrderEquals {
private int id;
private String orderName;
public int getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if(o instanceof OrderEquals){
OrderEquals orderEquals = (OrderEquals) o;
return this.id == orderEquals.id && this.orderName.equals(orderEquals.orderName);
}else{
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(id, orderName);
}
public OrderEquals(int id, String orderName) {
this.id = id;
this.orderName = orderName;
}
public void setId(int id) {
this.id = id;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
}
package com.java.object;
import java.util.Objects;
/**
* @author Admin
* @title: OrderEquals
* @projectName MyDemo
* @description: OrderEquals
* @date 2024/8/10 19:32
*/
public class OrderEquals {
private int id;
private String orderName;
public int getId() {
return id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if(o instanceof OrderEquals){
OrderEquals orderEquals = (OrderEquals) o;
return this.id == orderEquals.id && this.orderName.equals(orderEquals.orderName);
}else{
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(id, orderName);
}
public OrderEquals(int id, String orderName) {
this.id = id;
this.orderName = orderName;
}
public void setId(int id) {
this.id = id;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
}
public class OrderEqualsTest {
public static void main(String[] args) {
OrderEquals orderEquals1 = new OrderEquals(1,"张三");
OrderEquals orderEquals2 = new OrderEquals(1,"张三");
System.out.println(orderEquals1.equals(orderEquals2));
}
}
面试题:==和equals的区别
在Java中,"=="运算符和equals()方法之间的区别如下:
- "=="运算符:可以比较 基本数据类型 引用数据类型
- 它用于比较两个对象的引用(内存地址)是否相等。
- 如果两个对象引用相同的内存地址,则返回true;否则返回false。
- 对于基本数据类型,比较的是它们的值是否相等。对于引用数据类型(对象,数组等)比较是否指向同一个对象
- String == 比较的只是数据值 常量池
- equals()方法:只能比较引用数据类型
- 它是所有Java对象继承自Object类的一个方法。
- 默认情况下,equals()方法与"=="运算符的行为相同,即比较两个对象的引用是否相等。 地址值是否相等
- 但是,很多类(如String、Integer Date File等)会重写equals()方法,以实现逻辑上的只比较数据值相等性
- 通过重写equals()方法,可以自定义相等性的比较规则来判断两个对象的内容是否相等。
子类使用说明:
自定义的类在没有重写Object中equals()方法的情况下,调用的就是Object类中声明的equals(),比较两个
对象的引用地址是否相同。(或比较两个对象是否指向了堆空间中的同一个对象实体)
对于像String、File、Date和包装类等,它们都重写了Object类中的equals()方法,用于比较两个对象的
实体内容是否相等。
总结起来,在Java中,"=="运算符用于比较两个对象的引用是否相等,equals比较地址值是否相等。如果需要比较对象的内容,应当使用equals()方法,并且需要注意是否有针对该类的特殊重写。
idea可以自动重写equals方法 alt+ insert
2、(重点)toString()
方法签名:public String toString()
① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"
public String toString(){
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
② 在进行String与其它类型数据的连接操作时,自动调用toString()方法
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());
③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()
因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。
④ 可以根据需要在用户自定义类型中重写toString()方法 如String 类重写了toString()方法,返回字符串的值。
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());
子类使用说明:
自定义的类,在没有重写Object类的toString()的情况下,默认返回的是当前对象的地址值。
像String、File、Date或包装类等Object的子类,它们都重写了Object类的toString(),在调用toString()时,
返回当前对象的实体内容。
3、clone()
创建并返回当前对象的一个复制 == 判定是false
//Object类的clone()的使用
public class CloneTest {
public static void main(String[] args) {
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:" + a1);
a2.setName("毛毛");
System.out.println("clone之后的对象:" + a2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
class Animal implements Cloneable{
private String name;
public Animal() {
super();
}
public Animal(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal [name=" + name + "]";
}
@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
4、finalize()
jdk 9版本已过时 但仍可使用 销毁前调用 可以对其重写
弊端:可能导致内部出现循环引用导致此对象不能回收
- 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
- 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
- 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
- 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
- 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
- 在JDK 9中此方法已经被
标记为过时
的。
public class FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc();//强制性释放空间
}
}
class Person{
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
}
final finally finalize三者区别?
5、getClass()
public final Class<?> getClass():获取对象的运行时类型
因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}
6、hashCode()
public int hashCode():返回每个对象的hash值。
public static void main(String[] args) {
System.out.println("AA".hashCode());//2080
System.out.println("BB".hashCode());//2112
}
线程通信相关方法
notify() notifyAll() wait() wait(long timeoutMills) wait(long timeoutMills, int nanos)
native关键字的理解
使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++
等非Java语言实现的,并且被编译成了DLL
,由Java去调用。
-
本地方法是有方法体的,用c语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体
-
在Java中定义一个native方法时,并不提供实现体。
1. 为什么要用native方法
Java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,例如:Java需要与一些底层操作系统或某些硬件交换信息时的情况。native方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。
2. native声明的方法,对于调用者,可以当做和其他Java方法一样使用
native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。