失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Java(三)面向对象 封装继承多态 重写和重载 枚举

Java(三)面向对象 封装继承多态 重写和重载 枚举

时间:2020-09-01 13:14:01

相关推荐

Java(三)面向对象 封装继承多态 重写和重载 枚举

文章目录

一、面向对象与面向过程1.1 什么是面向对象1.2 面向对象与面向过程的区别 二、类与对象2.1 类中的内容2.1.1 成员变量和成员方法2.1.2 类中的全部内容2.1.3 局部变量和成员变量的区别 2.2 构造方法2.2.1 构造方法的使用2.2.2 构造代码块2.2.3 对象的存储2.2.4 匿名对象 三、抽象、封装、继承与多态3.1 抽象3.2 封装3.2.1 封装的优点3.2.2 封装的例子3.2.3 封装的层次 3.3 继承3.3.1 重写父类方法3.3.2 继承中的构造方法3.3.3 抽象类3.3.4 接口3.3.6 抽象类和接口的对比 3.4 多态3.5 封装继承多态的一些相关问题3.5.1 什么是多态机制?Java语言是如何实现多态的?【重要】3.5.2 在Java中定义一个空实现、无参构造方法的作用【重要】3.5.3在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?3.5.4 为什么Java不支持类多重继承3.5.5 接口的字段默认都是static和final的原因3.5.6 抽象类中构造方法的作用3.5.7 Java语言的三大特性 四、重写和重载4.1 重写4.2 重载4.3 重载和重写的比较4.4 说出几条方法重载的最佳实践 五、关键字5.1 所有关键字5.2 abstract5.3 final5.3.1 final的使用5.3.2 final、finally和finalize的区别5.3.3 final的好处5.3.4 final有哪些用法 5.4 this与super5.4.1 this5.4.2 super5.4.3 this与super的区别 5.5 static5.5.1 静态方法、静态变量和静态代码块5.5.2 类的初始化顺序5.5.3 static相关问题 六、枚举6.1 枚举的基本使用6.2 Enum类的常用方法6.3 枚举的实现6.3.1 Enum类中的方法6.3.2 枚举的实现

本系列文章:

Java(一)数据类型、变量类型、修饰符、运算符

Java(二)分支循环、数组、字符串、方法

Java(三)面向对象、封装继承多态、重写和重载、枚举

Java(四)内部类、包装类、异常、日期

Java(五)反射、克隆、泛型、语法糖、元注解

Java(六)IO、四种引用

Java(七)Lambda、Stream、新日期类、Optional

一、面向对象与面向过程

1.1 什么是面向对象

Java是一种面向对象程序设计(OOP)的语言

简而言之,面向对象中,实现一个功能的载体是对象,由对象去调用对应的方法即可。在面向过程中,某个功能的实现是由一系列函数组成的,某个函数中常常包含其他函数,由这些函数依次调用来实现复杂的功能。

要了解面向对象,需要先了解对象,对象的特点:

1.2 面向对象与面向过程的区别

用专业的话总结一下面向对象与面向过程的区别:

面向过程

一种较早的编程思想,顾名思义就是该思想是站着过程的角度思考问题,强调的就是功能行为,功能的执行过程,即先后顺序,而每一个功能我们都使用函数(类似于方法)把这些步骤一步一步实现,使用的时候依次调用函数就即可。面向对象

一种基于面向过程的新编程思想,顾名思义就是该思想是站在对象的角度思考问题,我们把多个功能合理放到不同对象里,强调的是具备某些功能的对象。

具备某种功能的实体,称为对象。面向对象最小的程序单元是:类。面向对象更加符合常规的思维方式,稳定性好,可重用性强,易于开发大型软件产品,有良好的可维护性。

在软件工程上,面向对象可以使工程更加模块化,实现更低的耦合和更高的内聚。

二、类与对象

类是一组事物共有特征和功能的描述。类是对于一组事物的总体描述,是用面向对象思想进行设计时最小的单位,也是组成项目的最基本的模块。对象是类的实例,类是对象的模板,如碳酸饮料是类,可口可乐、百事可乐、雪碧是具体的对象。

一个”.java”源文件中可以包含多个类(不是内部类),但一个源文件中最多只能有一个公开类(public class)而且文件名必须和公开类的类名完全保持一致。

2.1 类中的内容

2.1.1 成员变量和成员方法

类中一般至少两种内容:成员变量和成员方法成员变量也叫属性,是组成的对象的数据,比如Student对象,所具备的属性应该有姓名、各科目分数、所在班级、所在学校等;成员方法是操纵这些数据的行为,比如文理分科后调班、考试后取得了不同的分数等。

2.1.2 类中的全部内容

成员变量和成员方法是类中最常见的内容,但不是全部。类中所有可能的内容包括属性、方法、内部类、构造方法、代码块。属性和方法即刚介绍的成员变量和成员方法,内部类在本文第七小节有详细介绍,构造方法也很常见,是创建对象时要执行的方法,代码块在本文中第五小节和第六小节也有介绍,指的是{ }符号内的代码。

非静态成员变量的初始化位置一般有三处:声明该变量的时候初始化、构造方法中初始化、初始化块。

静态变量的初始化位置一般有两处:声明该属性的时候初始化、静态初始化块。

2.1.3 局部变量和成员变量的区别

成员变量和局部变量都能被final所修饰。

当对象通过new的方式被创建出来时,对象实体存在于堆,对象的成员变量在堆上分配空间,但对象里面的方法是没有出现的,只出现方法的声明,方法里面的局部变量并没有创建。等到对象调用此方法时,为了加快运行的速度,方法中的局部变量才会在栈中创建,所以,方法中的局部变量是在栈内的

2.2 构造方法

2.2.1 构造方法的使用

构造函数是创建对象时调用的方法,主要作用是给新创建的对象赋予一些初始变量(即完成对类对象的初始化工作)。当一个类中没有定义构造函数时,系统会默认给该类加入一个空参数的构造函数,当自己定义了构造函数后,就不再有默认的无参构造函数。

此处简单介绍,下文有详细说明。在 Java 中有多种方式可以创建对象,总结起来主要有下面的4种方式:

正常创建,通过new操作符。反射创建,调用Class或java.lang.reflect.Constructor的newInstance()方法。克隆创建,调用现有对象的clone()方法。反序列化,调用java.io.ObjectInputStream的getObject()方法反序列化。

构造函数的特点:

函数名与类名相同;无返回值,不能用void声明构造函数;生成类的对象时自动执行,无需调用。

在对象较为复杂时,常常有多个构造函数,这些构造函数是以重载的形式存在的。

2.2.2 构造代码块

在构造函数调用之前,还可以执行一些代码,这些代码称为构造代码块。包含构造代码块的实体类代码:

public class Thing {/*构造代码块*/{System.out.println("做事前做一些准备工作");}public Thing(){System.out.println("开始做事"); }}

测试代码:

public class ThingTest {public static void main(String[] args){Thing thing = new Thing();}}

测试结果:

做事前做一些准备工作

开始做事

2.2.3 对象的存储

当使用new的方式创建对象时,会先在堆内存中开辟一块区域,存放该对象的具体数据(即成员变量),然后在栈内存中生成一个引用,指向堆内存中生成的具体对象。如下:

需要注意的是类变量(静态变量)存在于方法区。

关于不同内存区域的特点,此处简单介绍,后续会有专门的文章进行分析,区别:

2.2.4 匿名对象

在Java中,有时会创建匿名对象,匿名对象就是没有名字的对象,在创建对象时,只通过new的动作在堆内存开辟空间,却没有把堆内存空间的地址值赋值给栈内存的某个变量用以存储。

由于使用匿名对象不需要分配栈内存,且无需进行引用指向,在大量创建对象的时候能够节约很多的栈空间,且数量越多越明显。

使用匿名对象的好处是:当匿名对象使用完毕就是垃圾,垃圾回收器会在空闲时对匿名对象进行回收,节省栈内存空间

匿名对象的使用场景常常有两种:

1、仅仅只调用一次的时候

示例:

public class Worker {public void say(){System.out.println("我要当个有追求的程序员");}}public class BasicTest {public static void main(String[] args) {new Worker().say();}}

2、作为参数传递

示例:

public class Book {private String name;public Book(String name){this.name=name;}public String getName(){return this.name; }public void setName(String name){this.name=name;}}public class Worker {public void say(Book book){System.out.println("我最近在看"+book.getName());}}public class BasicTest {public static void main(String[] args) {Worker wooker = new Worker();wooker.say(new Book("Java多线程编程实战指南"));}}

三、抽象、封装、继承与多态

3.1 抽象

抽象是将一类对象的共同特征总结出来构造类的过程, 包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。

3.2 封装

在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部分包装、隐藏起来的方法。

3.2.1 封装的优点

 封装的优点:

1、隔离变化,便于使用

此处常见的是:某个类中某一功能的实现,该类自己知道就行了,即便该功能的实现方式变化了,外部调用者也仍不知道该功能实现的细节,这样就达到了“隐藏细节,隔离变化的目的”。2、提高代码复用性

这个好处较容易理解,当开发者A开发了一个复杂的功能,封装到functionA中,那么开发者B就可以直接调用functionA方法,不用再重新实现该功能。3、提高安全性

此类好处,最常见的例子是对私有属性的封装。

3.2.2 封装的例子

此处用一个例子来说明封装后提高安全性的作用,用Student类来表示学生,示例:

public class Student {String name;int grade;}public class BasicTest {//在测试类中可以给学生修改成绩public static void main(String[] args){Student student = new Student(); student.grade=-10;System.out.println("该学生的分数是:"+student.grade); //该学生的分数是:-10}}

该结果明显是不对的,学生的成绩不应该为负数。此时的修改常常有两个方面:将私有属性用private修饰,然后添加getter和setter方法,在setter方法中添加判断参数的逻辑。修改后的Student示例代码:

public class Student {private String name;private int grade;public Student(){}public String getName(){return this.name;}public void setName(String name){if(name.length()!=0){this.name = name;}}public int getGrade(){return this.grade;}public void setGrade(int grade){if(grade > 100 || grade < 0){System.out.println("分数参数不合法,应该是0-100的整数");}else{this.grade = grade;}}}

修改后的测试代码:

package Basic;public class BasicTest {public static void main(String[] args){Student student = new Student(); student.setGrade(-10);//分数参数不合法,应该是0-100的整数System.out.println("该学生的分数是:"+student.getGrade()); student.setGrade(90);//该学生的分数是:90System.out.println("该学生的分数是:"+student.getGrade()); }}

从上面结果可以看出,修改后的代码可以检测非法参数,通过用调用方法而不是直接修改属性的方式来修改学生的分数,达到了提高Student类成员属性安全性的目的。

3.2.3 封装的层次

Java中的封装体有三种形式:

1、方法

最简单的封装体,方法有4种修饰符:default、private、protected和public,这4种修饰符的特点不再赘述。

2. 类

可通过访问修饰符进行隐藏。

3. 包

一般一个包(package)里是一系列相关功能模块的封装,使用别的包里的方法时,需要用import关键字。

3.3 继承

Java支持单继承,继承最大的优点是提高代码复用性,通常的做法是父类中定义公共的共性功能,不同的子类实现不同的差异功能。

子类是父类的扩展(extends)。

所有类的公共父类是Object。

继承是最简单,也是最常见的类与类之间的一种关系。

3.3.1 重写父类方法

当父类中的方法,并不完全适用于子类时,子类可以重写父类的方法。重写时,需要保证方法名称、参数都和父类一致。示例:

/*父类:手机*/public class Phone {public void playRingtone(){System.out.println("播放手机默认铃声 "); }}/*子类:荣耀手机*/public class HonorPhone extends Phone{@Overridepublic void playRingtone(){System.out.println("播放手机铃声《荣耀》 "); }}/*测试类*/public class PhoneTest {public static void main(String[] args){HonorPhone honorPhone = new HonorPhone();honorPhone.playRingtone(); //播放手机铃声《荣耀》 }}

在重写父类的方法,需要注意以下几点:

子类重写父类方法,必须保证子类方法的权限要大于或等于父类权限,才可以重写。继承当中子类抛出的异常必须是父类抛出的异常的子异常,或者子类抛出的异常要比父类抛出的异常要少。如果返回值为引用类型,其返回值类型必须与父类返回值类型相同或为父类返回值类型的子类

可以简单理解为:权限放大,返回值和异常缩小

3.3.2 继承中的构造方法

关于子类的构造方法,在对子类对象进行初始化时,父类构造函数也会运行,是因为子类的构造函数默认第一行有一条隐式的语句super()。此处可以将上面的示例代码改下,来查看效果,示例:

/*父类:手机*/public class Phone {public Phone(){System.out.println("创建手机 ");}public void playRingtone(){System.out.println("播放手机默认铃声 "); }}/*子类:荣耀手机*/public class HonorPhone extends Phone{public HonorPhone(){System.out.println("创建荣耀手机 ");}public void playRingtone(){System.out.println("播放手机铃声《荣耀》 "); }}/*测试类*/public class PhoneTest {public static void main(String[] args){HonorPhone honorPhone = new HonorPhone();honorPhone.playRingtone();}}

测试结果:

创建手机

创建荣耀手机

播放手机铃声《荣耀》

此处需要注意的是:作为子类,无论如何都会调用父类的构造方法。默认情况下,会调用父类的无参的构造方法

上面的规则就会导致一个现象:当父类没有无参构造方法的时候( 提供了有参构造方法,并且不显示提供无参构造方法),子类就会抛出异常,因为它尝试去调用父类的无参构造方法。此时,必须通过super去调用父类声明的、存在的、有参的构造方法 。

3.3.3 抽象类

在父类中定义一个方法时,可以实现一个较完整的功能,子类不重写也能完全使用。当然,父类也可以完全不实现或者部分实现某个功能,此时父类需要子类去重写这个功能,对应实现功能的方法就要用abstract关键字来标识,此时的类就叫抽象类。

抽象类的特点:

1>抽象方法一定在抽象类中。

2>抽象方法和抽象类都必须被abstract关键字修饰。

3>抽象类不可以用new创建对象。

4>抽象类中的抽象方法要想被使用,必须由子类复写其所有的抽象方法后,建立子类对象调用,如果子类只覆盖了部分抽象方法,那么该子类还是一个抽象类。

一般情况下,抽象类就是将一些父类中完全不确定或部分不确定的部分抽取出来,封装到abstract方法中,让子类去实现。当然,抽象类也可以不定义抽象方法,这样只是为了不让该类创建对象。

抽象类可以有构造方法,但是该类又不能实例化,这样做是为了什么呢?其实只是让子类调用。

由上述内容可知抽象方法与抽象类的关系:有抽象方法存在的类一定是抽象类,抽象类中不一定有抽象方法(这样做,只是为了防止抽象类实例化)

3.3.4 接口

实际开发中,对于父类未实现、要子类实现方法的形式,常常接口用的更多一些,而不是抽象类。接口是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。在接口中,所有的方法都是抽象的、没有任何方法体的,其特点如下:

1>接口中一般有两种内容:常量,方法声明。

2>接口中的成员都有固定修饰符(常量:public static final;方法:public abstract)

接口也是不能直接通过new创建对象的,因为接口中的方法都是抽象的,而抽象方法需要被子类实现。当子类对接口中的抽象方法全都重写后,子类才可以实例化。

Java支持单继承,多实现。这种实现方式的原因是:不能多继承,是避免不同父类中具有相同的方法,子类重写该方法时,就会引起歧义,不能确定是重写的哪个父类总的方法。而实现不会有此问题,因为不同接口中有的只是方法的声明,都没具体实现,不存在歧义。

如果不同的接口A、B有相同的接口声明,实现类实现一次即可

从子类和父类的角度考虑,类和接口之间的关系:

抽象类可以实现接口,用来实现接口中的部分方法

接口与接口之间可以多继承,两个父接口示例:

public interface InterfaceA {void play();}public interface InterfaceB {void study();}

继承两个父接口的子接口示例:

public interface InterfaceC extends InterfaceA,InterfaceB {void work();}

上面的代码更多的是从形式上介绍接口的特点,从总体上看的话,接口特点如下:

接口是对外暴露的规则。个人理解,此处的规则,指的是接口定义的方法声明。要实现某个接口,就必须要重写接口里特定的方法,这个特定的方法就是接口对外的规则。接口是程序的功能扩展。接口往往代表某种较为单一方面/模块的功能,某个类只能继承一个父类,要想实现较多维度的功能,就需要用到接口。接口的出现降低耦合度。此处指的是Java中的向上转型,也就是父类/接口的引用可以指向子类的对象,这样在传参时就不用强制指定是哪个子类对象,传入父类/接口的引用即可,降低了耦合度。接口可以用来多实现。这个较容易理解,一个类可以实现多个接口。类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口接口与接口直接可以有继承关系,且接口可以多继承

3.3.6 抽象类和接口的对比

抽象类是用来捕捉子类的通用特性的,接口是抽象方法的集合。

从设计层面来说,抽象类是对类的抽象,是一种模板设计;接口是行为(能力)的抽象,是一种行为(能力)的规范

抽象类和接口的相同点:

1、接口和抽象类都不能实例化

2、都位于继承的顶端,用于被其他实现或继承

3、都包含抽象方法,其子类(假如抽象类的子类不是抽象类的话)都必须覆写这些抽象方法。

抽象类和接口的不同点:

在进行抽象类和接口的选择时,有一种角度是:is a 某种事物的关系,可以用抽象类;has a 某种功能的关系,可以用接口。

Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。所以,在JDK1.8之后,就可以为接口提供默认实现的方法了,并且不用强制子类来实现它。

接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:

行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类。选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能。

3.4 多态

多态指的是同一个行为具有多个不同表现形式或形态(调用同一个方法,不同的对象有不同的行为)的能力。扩展开来说,多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。这话有点抽象,先上例子:

/*家庭作业接口*/public interface Homework {void doHomework();}/*两个实现类*/public class StudentA implements Homework{@Overridepublic void doHomework() {System.out.println("同学A写了一篇作文");}}public class StudentB implements Homework{@Overridepublic void doHomework() {System.out.println("同学B写了一套数学试卷");}}/*测试类*/public class StudentTest {public static void main(String[] args){Homework homeworkA = new StudentA();homeworkA.doHomework(); //同学A写了一篇作文Homework homeworkB = new StudentB();homeworkB.doHomework(); //同学B写了一套数学试卷}}

从上面的例子可以看出,同一个Homework接口类型的引用,调用同样的方法,却产生了不同的结果,这就是多态最直接的体现。所以,多态最常见的方式就是:父类的引用指向了自己的子类对象。

这个例子中也能看出多态存在的三个条件:继承、重写、父类引用指向子类对象

多态是为了提高代码的可扩展性和维护性,方便代码透明地编写。

实现多态的两种方式:

1)使用父类作为方法形参

2)使用父类作为方法返回值

3.5 封装继承多态的一些相关问题

3.5.1 什么是多态机制?Java语言是如何实现多态的?【重要】

所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。

多态分为编译时多态运行时多态。其中编辑时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的函数,通过编辑之后会变成两个不同的函数,在运行时谈不上多态。而运行时多态是动态的,它是通过动态绑定来实现的,也就是我们所说的多态性

Java实现多态有三个必要条件:

只有满足了上述三个条件,才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而达到执行不同的行为。

对于Java而言,它多态的实现机制遵循一个原则:当超类对象引用变量引用子类对象时,被引用对象的类型而不是引用变量的类型决定了调用谁的成员方法,但是这个被调用的方法必须是在超类中定义过的,也就是说被子类覆盖的方法。

3.5.2 在Java中定义一个空实现、无参构造方法的作用【重要】

Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“没有参数的构造方法”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到没有参数的构造方法可供执行。解决办法是在父类里加上一个不做事且没有参数的构造方法。

3.5.3在调用子类构造方法之前会先调用父类没有参数的构造方法,其目的是?

帮助子类做初始化工作。

3.5.4 为什么Java不支持类多重继承

Java不支持类多重继承, 可以考虑以下两点:

1、钻石形继承问题产生的歧义

考虑一个类 A 有 foo() 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现,现在 D 类使用多个继承派生自 B 和 C。如果我们只引用 foo(), 编译器将无法决定它应该调用哪个 foo(),这也称为 Diamond 问题,因为这个继承方案的结构类似于菱形。示例:

2、多重继承确实使设计复杂化并在强制转换、构造函数链接等过程中产生问题

Java 可以通过使用接口支持多继承来避免这种歧义。由于接口只有方法声明而且没有提供任何实现,因此只有一个特定方法的实现,因此不会有任何歧义。

3.5.5 接口的字段默认都是static和final的原因

接口中如果定义非final的变量的话,而方法又都是abstract的,变量的修改就需要实现类来完成。这样一来就有悖于Sun公司开始设计interface的初衷。interface就像一个合同,规定后来的类A和B, 都按照这个合同来做事,怎么能谁想怎么改就怎么改?另外,为什么必须为static呢?这样,未来的子类的static方法也能访问到它,可以最大限度的发挥接口属性的功能。

3.5.6 抽象类中构造方法的作用

抽象类可以声明并定义构造函数。不过因为不可以创建抽象类的实例,所以构造函数只能通过构造函数链调用(Java中构造函数链指的是从其他构造函数调用一个构造函数),比如:抽象类的子类。也就是说,虽然抽象类自身不能实例化,但当抽象类的子类实例化时,还是会调用抽象类中的构造方法,此时抽象类构造方法初始化一些参数的作用就体现出来了

如果不能对抽象类实例化,那么构造函数的作用是什么?

它可以用来初始化抽象类内部声明的通用变量。另外,即使你没有提供任何构造函数,编译器将为抽象类添加默认的无参数的构造函数,没有的话你的子类将无法编译,因为在任何构造函数中的第一条语句隐式调用super(),Java中默认超类的构造函数。

3.5.7 Java语言的三大特性
1、封装

属性可以用来描述同一类事物的特征,方法可以描述一类事物可做的操作。封装就是把属于同一类事物的共性(包括属性与方法)归到一个类中,以方便使用。2、继承

一个类继承另一个类,则称继承的类为子类,被继承的类为父类。继承的目的是为了代码的复用,同时也方便了子类的功能扩展。3、多态

父类引用指向的不同变量,在调用相同方法时可能有不同的行为。

四、重写和重载

4.1 重写

重写:顾名思义,就是在子类中,把父类中不满足要求的方法再重写(重写实现)一遍。在重写时,常见的形式是:子类中的方法名、参数列表、返回类值和父类中的均相同。示例:

/*父类:手机*/public class Phone {public void playRingtone(){System.out.println("播放手机默认铃声 "); }}/*子类:荣耀手机*/public class HonorPhone extends Phone{@Overridepublic void playRingtone(){System.out.println("播放手机铃声《荣耀》 "); }}

上面的例子只是最常见的形式,在重写时,还有许多要注意的地方。重新需要注意的11条规则:

1、参数列表必须完全与被重写方法完全相同;

2、当返回值为基本数据类型时,返回值类型必须完全与被重写方法的返回类型相同;当返回值为引用类型时,重写方法的返回值类型要<=父类中返回值的类型;

3、访问权限不能比父类中被重写的方法的访问权限更低;

4、父类的成员方法只能被它的子类重写;

5、声明为final的方法不能被重写;

6、声明为static的方法不能被重写,但可以被再次声明;

7、子类和父类在同一个包中,那么子类可以重写父类中所有方法,除了声明为private和final的方法;

8、子类和父类不在同一个包中,那么子类只能重写父类中声明为public和protected的非final方法;

9、重写的方法不能抛出父类方法中不存在的异常,或者比被重写方法声明的更广泛的强制性异常;

10、构造方法不能被重写;

11、无法以返回值类型作为重载函数的区分标准。

1、参数列表必须完全与被重写方法完全相同

该条规则指的是重写的方法和父类中的原有方法的方法名、参数列表及参数列表的顺序完全一样才行,这三样也被方法签名。如果两个方法的方法签名不一样,我们就认为这两个方法不能构成重写关系。

2、当返回值为基本数据类型时,返回值类型必须完全与被重写方法的返回类型相同;当返回值为引用类型时,重写方法的返回值类型要<=父类中返回值的类型【重要】

上代码:

/*父类:手机*/public class Phone {String name;public Phone getPhone(){Phone phone = new Phone();phone.name = "Phone";return phone;}}/*子类:荣耀手机*/public class HonorPhone extends Phone{@Overridepublic HonorPhone getPhone(){HonorPhone honorPhone = new HonorPhone();honorPhone.name = "HonorPhone";return honorPhone;}}/*测试类*/public class PhoneTest {public static void main(String[] args){HonorPhone honorPhone = new HonorPhone();Phone phone = new Phone();phone = honorPhone.getPhone();System.out.println(phone.name);phone = phone.getPhone();System.out.println(phone.name);}}

测试结果如下:

HonorPhone

HonorPhone

初看这个结果,挺意外的,怎么不是一个Phone、一个HonorPhone?此处就要提到一个Java中一个常用的概念向上造型,即父类的引用指向子类的对象。看到这个,再理解这段代码,应该不就难了。测试类中前两句是创建了一个子类HonorPhone对象和父类Phone对象。从第三句开始向上造型,phone变量指向的就是子类对象,所以此时调用phone.getPhone()其实是调用的子类HonorPhone中的getPhone方法,输出结果自然也是"HonorPhone"。

回到最初的问题:为什么子类重写方法的引用类型可以是父类中方法的子类?因为这样可以更好地使用向上造型,更方便地调用子类的方法。

3、访问权限不能比父类中被重写的方法的访问权限更低【重要】

此处要提的是设计模式中用到的一个原则:里氏代换原则(任何基类可以出现的地方,子类一定可以出现)。如果子类的访问权限比父类小的话,在使用向上造型时,可能会出现父类引用访问不了子类接口的错误。我们就将上面用到的例子稍微改改,将子类HonorPhone中getPhone的权限改成private,此时就能看能代码报错:

4、父类的成员方法只能被它的子类重写

这句话较容易理解,如果两个签名相同的方法不是出现在继承关系的类中,他们也不会构成重写关系。5、声明为final的方法不能被重写

这句话也容易理解,这是final关键字的性质。6、声明为static的方法不能被重写,但可以被再次声明

静态方法可以被继承,但不能被重写。如果父类中定义的静态方法在子类中被重新声明,那么在父类中定义的静态方法将被隐藏,不是重写。此时子类和父类中两个静态方法并不存在任何关系。调用方法如下:7、子类和父类在同一个包中,那么子类可以重写父类中所有方法,除了声明为private和final的方法8、子类和父类不在同一个包中,那么子类只能重写父类中声明为public和protected的非final方法

规则7和规则8可以放在一起理解,private方法和final方法不能被重写。至于访问修饰符,个人理解,能访问的才可能被重写。9、重写的方法不能抛出父类方法中不存在的异常,或者比被重写方法声明的更广泛的强制性异常10、构造方法不能被重写

构造方法不能被继承,所以也就不能被重写。11、如果不能继承一个方法所在的父类,则不能重写这个方法

规则11和规则4可以放在一起理解,如果两个方法所以的类不存在继承关系,则谈不上重写。

4.2 重载

重载指的是一个类/子类中,方法名相同,方法参数个数 / 参数类型 / 参数顺序不同的情况。重载时需要遵守的6个规则如下:

1、方法能够在同一个类中或者在一个子类中被重载

常见的重载是出现在同一个类中的,但其实子类从父类继承一个方法,定义一个同名异参的方法也是重载。2、被重载的方法,必须改变参数列表(参数个数或参数类型或参数顺序)

这个也容易理解,除了方法名,方法签名的其他位置变化都可以造成重载。3、被重载的方法可以改变返回类型

这条规则可以和规则6放在一起说,返回值类型不在方法签名里,所以在重载时可以改变,但不足以作为重载的区分标准。4、被重载的方法可以声明新的或更广的检查异常

重载的方法,从某种程度上,可以理解为:两个互不想干的方法,只是恰好方法名相同而已,所以异常检查范围自然可以不一样。5、被重载的方法可以改变访问修饰符

修饰符不在方法签名里,所以在重载时可以改变。6、无法以返回值类型作为重载函数的区分标准

4.3 重载和重写的比较

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态 性,而后者实现的是运行时的多态性。

重载

发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不 同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。

重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理重写

发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛 出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类 方法访问修饰符为private则子类中就不是重写。

重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变重载和重写的区别

4.4 说出几条方法重载的最佳实践

下面有几条可以遵循的方法重载的最佳实践来避免造成自动装箱的混乱。

1)不要重载这样的方法:一个方法接收int参数,而另个方法接收Integer参数。

2)不要重载参数数量一致,而只是参数顺序不同的方法。

3)如果重载的方法参数个数多于5个,采用可变参数。

五、关键字

Java关键字的标准定义是电脑语言里事先定义的,有特别意义的标识符,有时又叫保留字,还有特别意义的变量。Java的关键字对Java的编译器有特殊的意义,他们用来表示一种数据类型,或者表示程序的结构等,关键字不能用作变量名、方法名、类名、包名和参数

5.1 所有关键字

java关键字共53个,包含两个保留字const,goto,具体:

5.2 abstract

abstract代表抽象,可以修饰类和方法。

1、抽象方法

abstract修饰方法时,代表抽象方法。抽象方法的特点:

抽象方法没有自己的方法体;抽象方法不能用private修饰,因为抽象方法必须被子类实现(覆写),而private权限对于子类来说是不能访问的,所以就会产生矛盾;抽象方法也不能用static修饰,如果用static修饰了,就可以直接通过类名调用,而抽象方法压根就没有任何实现,这样的调用是没有意义的
2、抽象类

abstract修饰类时,代表抽象类。抽象类的特点:

抽象类不能被实例化,也就是说我们没法直接new一个抽象类,抽象类只能由它的继承类实例化;抽象类虽然不能被实例化,但有自己的构造方法;抽象类与接口有很大的不同之处,接口中不能有实例方法去实现业务逻辑,而抽象类中可以有实例方法,并实现业务逻辑,比如可以在抽象类中创建和销毁一个线程池;抽象类不能使用final关键字修饰,因为final修饰的类是无法被继承,而对于抽象类来说就是需要通过继承去实现抽象方法,这又会产生矛盾。
3、抽象类和抽象方法的关系如果一个类中至少有一个抽象方法,那么这个类一定是抽象类,但反之则不然。也就是说一个抽象类中可以没有抽象方法。这样做的目的是为了此类不能被实例化。如果一个类继承了一个抽象类,那么它必须全部覆写抽象类中的抽象方法,当然也可以不全部覆写,如果不覆写全部抽象方法则这个子类也必须是抽象类。

5.3 final

5.3.1 final的使用

final代表最终的、不可更改的,可以修饰变量、方法和类。

final修饰变量时, 如果引用为基本数据类型,则该引用为常量,该值无法修改(更准确的说法:该变量只有一次赋值的机会);如果引用为引用数据类型,比如对象、数组,则该对象、数组本身可以修改,但指向该对象或数组的地址的引用不能修改;如果引用时类的成员变量,则必须当场赋值,否则编译会报错。

当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。

final修饰方法时,表示该方法无法被子类重写,但是可以被继承。

final修饰类时,表示该类无法被继承,同时类中的所有成员方法都会被隐式定义为final方法(只有在需要确保类中的所有方法都不被重写时才使用final修饰类)。

5.3.2 final、finally和finalize的区别
final

可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。finally

一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。finalize

是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

5.3.3 final的好处
final关键字提高了性能。JVM和Java 应用都会缓存final变量。final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。使用final关键字,JVM会对方法、变量及类进行优化。
5.3.4 final有哪些用法
1、被final修饰的类不可以被继承。2、被final修饰的方法不可以被重写。3、被final修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。4、被final修饰的方法,JVM会尝试将其内联,以提高运行效率。5、被final修饰的常量,在编译阶段会存入常量池中。

除此之外,编译器对final域要遵守的两个重排序规则:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

5.4 this与super

5.4.1 this

this表示当前对象。

this访问本类中的属性,如果本类没有此属性则从父类中继续查找

this访问本类中的方法,如果本类没有此方法则从父类中继续查找

this调用本类构造,必须放在构造方法的首行。super调用父类构造,必须放在子类构造方法首行

this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

this的用法在java中大体可以分为3种(常见的是后两种):

1、普通的直接引用

this相当于是指向当前对象本身。2、形参与成员名字重名,用this来区分

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

3、引用本类的构造函数

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

5.4.2 super

super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。super也有三种用法:

1、普通的直接引用

与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。2、子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区

示例:

class Person{protected String name;public Person(String name) {this.name = name;}}class Student extends Person{private String name;public Student(String name, String name1) {super(name);this.name = name1;}public void getInfo(){System.out.println(this.name); //ChildSystem.out.println(super.name); //Father}}public class Test {public static void main(String[] args) {Student s1 = new Student("Father","Child");s1.getInfo();}}

3、引用父类构造函数

super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。

this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

5.4.3 this与super的区别

使用this与super需要注意的地方:

1、super()和this()均需放在构造方法内第一行。2、this和super不能同时出现在一个构造函数里面,因为this一般会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。3、this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。

5.5 static

static关键字主要用来修饰成员变量和成员方法,《Java编程思想》对static的描述:

static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。

简言之,使用static的目的是为了在不创建对象的前提下来调用方法/变量

5.5.1 静态方法、静态变量和静态代码块
1、静态方法

static修饰方法时,该方法称为静态方法。在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的2、静态变量

static修饰变量时,称为静态变量。静态变量在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。static成员变量的初始化顺序按照定义的顺序进行初始化。3、静态代码块

除了修饰变量和方法外,static还可以修饰代码块,称为静态代码块。一个类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。为什么说static块可以用来优化程序性能,是因为它的特性:只会在类加载的时候执行一次

5.5.2 类的初始化顺序

类的初始化顺序(先父类,后子类先静态,后实例;先变量,后代码块,最后构造函数):

父类静态变量父类静态代码块子类静态变量子类静态代码块父类普通变量父类普通代码块父类构造函数子类普通变量子类普通代码块子类构造函数

关于该初始化顺序,可以通过如下测试代码来验证。父类测试代码:

package Basic;public class BaseClass {/*父类静态变量*/ public static String baseStaticVariable = "父类-->静态变量";/*父类普通变量*/ public String baseVariable = "父类-->普通变量"; /*静态代码块*/ static{System.out.println(baseStaticVariable); System.out.println("父类-->静态代码块"); } /*父类构造函数*/ public BaseClass() {System.out.println("父类-->构造函数"); } /*普通代码块*/ {System.out.println(baseVariable); System.out.println("父类-->普通代码块"); } }

子类测试代码:

package Basic;public class SubClass extends BaseClass{/*子类静态变量*/ public static String subStaticVariable = "子类-->静态变量";/*子类普通变量*/ public String subVariable = "子类-->普通变量"; public static void main(String[] args) {new SubClass(); } /*静态代码块*/ static {System.out.println(subStaticVariable); System.out.println("子类-->静态代码块"); } /*普通代码块*/ {System.out.println(subVariable); System.out.println("子类-->普通代码块"); } /*子类构造函数*/ public SubClass() {System.out.println("子类-->构造函数"); } }

测试结果:

父类–>静态变量

父类–>静态代码块

子类–>静态变量

子类–>静态代码块

父类–>普通变量

父类–>普通代码块

父类–>构造函数

子类–>普通变量

子类–>普通代码块

子类–>构造函数

5.5.3 static相关问题

1、static关键字特点

 1、static是一个修饰符,用于修饰成员(成员变量,成员函数)。static修饰的成员变量 称之为静态变量或类变量。

 2、static修饰的成员被所有的对象共享。

 3、static优先于对象存在,因为static的成员随着类的加载就已经存在。

 4、static修饰的成员多了一种调用方式,可以直接被类名所调用(调用方式:类名.静态成员)。

 5、static修饰的数据是共享数据,对象中的存储的是特有的数据。

2、成员变量和静态变量的区别

3、静态使用时需要注意的事项

 1、静态方法只能访问静态成员(非静态既可以访问静态,又可以访问非静态)。

 2、静态方法中不可以使用this或者super关键字。

 3、主函数是静态的。4、static方法是否能被覆盖

 static方法是不可以被覆盖的。因为方法覆盖是基于运行时动态绑定的,而static方法是翻译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。5、static成员和static方法特点

 1、生命周期和类一样。类一加载到内存中时,static就已经加载到方法区中,随着类的结束调用而结束。优先于对象存在,static成员和static方法是先存在的,一般用来初始化一些所有对象都会用到的属性和方法;对象是后存在的,可以直接使用这些static变量和方法。

 2、被所有对象所共享。非static变量是属于某个具体的对象,而static变量是属于类的。

 3、可以直接被类名调用6、使用static的优缺点

 优点:对对象的共享数据进行单独空间的存储,节省空间。没有必要每一个对象中存储一份。

 缺点:生命周期过长(可能会有垃圾不能被回收);访问出现局限性(静态虽好,但只能访问静态)。7、抽象方法是否可以同时是静态的?

 不能,静态方法不能重写8、static可否用来修饰局部变量?

 不能。9、Java中的编译期常量是什么?使用它又什么风险?

 公共静态不可变(public static final )变量也就是我们所说的编译期常量,这里的public可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的JAR。为了避免这种情况,当你在更新依赖JAR文件时,确保重新编译你的程序。10、父类的静态方法能否被子类重写?静态属性和静态方法是否可以被继承?

 父类的静态方法和属性不能被子类重写,但子类可以继承父类静态方法和属性,如父类和子类都有同名同参同返回值的静态方法show(),声明的实例Father father = new Son();(Son extends Father),会调用father对象的静态方法。静态是指在编译时就会分配内存且一直存在,跟对象实例无关。

六、枚举

6.1 枚举的基本使用

枚举是一个整型常数的集合,用于声明一组带标识符的常数,常用于整数数量固定的场景,比如月份、星期、性别等。JDK1.5中引入了枚举,其语法为:

修饰符 enum 枚举名称:枚举变量类型{枚举成员}

需要注意的是:

1、枚举变量类型可以不写,默认的是int型,具体的枚举变量对应的值从0开始,逐渐递增;2、任意两个枚举成员不能具有相同的名称,且它的常数值必须在该枚举的基础类型的范围之内,多个枚举成员之间使用逗号分隔;3、引用枚举变量时的格式是:枚举名称.枚举变量

总的来说,除了不能继承,基本上可以将 enum 看做一个常规的类

看个具体的枚举的例子:

public enum Color {RED,BLUE,GREEN,BLACK;}public class EnumTest {public static void main(String[] args) {Color color = Color.BLACK;switch(color){case BLACK:System.out.println("黑色");break;case RED:System.out.println("红色");break;case GREEN:System.out.println("绿色");break;case BLUE:System.out.println("蓝色");break;}}}

测试结果为:

黑色

枚举类稍微高级的用法:

public enum EventEnum {LAUNCH("launch"),PAGEVIEW("pageview"),EVENT("event");EventEnum(String name){this.name = name;}private String name;public void show(){System.out.println(this.name);EventEnum[] ee = values();for(int i = 0;i<ee.length;i++){System.out.println(ee[i]);}}}public class Test {public static void main(String[] args) {EventEnum ee = EventEnum.LAUNCH;ee.show();String name = EventEnum.PAGEVIEW.name();System.out.println(name);}}

枚举更常见的做法是定义code和description2个成员变量。示例:

public enum TypeEnum{LOAN(1,"loan"),REPAY(2,"repay");private Integer status;private String msg;TypeEnum(Integer status,String msg){this.status = status;this.msg = msg;}public Integer getStatus(){return status;}public String getMsg(){return msg;}public static String getNameByKey(String dictValue){for(TypeEnum enums:TypeEnum.values()){String code = String.valueOf(enums.getStatus());if (code.equals(dictValue))return enums.getMsg;}return "";}}

6.2 Enum类的常用方法

枚举继承自java.lang.Enum 类,所以枚举不能再继承其他类。当定义一个枚举类型时,每一个枚举类型成员都可以看作是 Enum 类的实例。Enum类的常用方法如下:

1、values()

该方法作用是:以数组形式返回枚举类型的所有成员,数组中元素的顺序就是在枚举中声明的顺序。

仍以上面的Color枚举为例,测试代码改为:

for(int i=0;i<Color.values().length;i++){System.out.println(Color.values()[i]);}

测试结果为:

RED

BLUE

GREEN

BLACK

2、valueOf()

该方法作用是:将普通字符串转换为枚举实例。3、compareTo()

该方法作用是:比较两个枚举成员在定义时的顺序。

将valueOf和compareTo方法联合起来使用,示例:

public static void main(String[] args) {compare(Color.valueOf("RED"));}public static void compare(Color color){for(int i=0;i<Color.values().length;i++){System.out.println(color+"与"+Color.values()[i]+"的比较结果是:"+pareTo(Color.values()[i]));}}

测试结果为:

RED与RED的比较结果是:0

RED与BLUE的比较结果是:-1

RED与GREEN的比较结果是:-2

RED与BLACK的比较结果是:-3

4、ordinal()

该方法作用是:获取枚举成员的索引位置。

示例:

for(int i=0;i<Color.values().length;i++){System.out.println("索引"+Color.values()[i].ordinal()+",值:"+Color.values()[i]);}

测试结果为:

索引0,值:RED

索引1,值:BLUE

索引2,值:GREEN

索引3,值:BLACK

6.3 枚举的实现

6.3.1 Enum类中的方法

Enum类是一个抽象类,主要有name和ordinal两个属性,分别用于表示枚举元素的名称和枚举元素的位置索引。

Enum类中的一些方法:

//返回namepublic final String name()//返回ordinalpublic final int ordinal()//返回namepublic String toString()//直接用==比较两个对象public final boolean equals(Object other)//调用的是父类的 hashCode 方法public final int hashCode()//比较 ordinal 的大小public final int compareTo(E o)//根据传入的字符串name来返回对应的枚举元素public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name)

6.3.2 枚举的实现

例子:

public enum Labels0 {ENVIRONMENT("环保"), TRAFFIC("交通"), PHONE("手机");private String name;private Labels0(String name) {this.name = name;}public String getName() {return name;}}

编译后生成的字节码反编译:

枚举被编译后其实就是一个类,该类被声明成 final,说明其不能被继承,同时它继承了Enum类。枚举里面的元素被声明成static final,另外生成一个静态代码块static{},最后还会生成values和valueOf两个方法。

如果觉得《Java(三)面向对象 封装继承多态 重写和重载 枚举》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。