面向对象 ( Object Oriented Programming )
概述
面向对象是一种编程思想,相对于面向过程。
- 面向过程:分析解决问题说需要的步骤,然后用函数把这些步骤一一实现,使用的时候一个一个依次调用。
- 面向对象:把要解决的问题按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题
面向对象,更多的是要进行子模块化的设计,每一个模块都需要单独存在,并且可以被重复利用,所以面向对象的开发更像是一个具备标准的开发模式。(本质上来讲就是一种组件化(模块化)的设计,方便代码局部维护。)
理解面向过程(OPP)、面向对象(OOP)、面向切面(AOP)
-
面向过程编程OPP:Procedure Oriented Programming,是一种以事物为中心的编程思想。主要关注“怎么做”,即完成任务的具体细节。
-
面向对象编程OOP:Object Oriented Programming,是一种以对象为基础的编程思想。主要关注“谁来做”,即完成任务的对象。
-
面向切面编程AOP:Aspect Oriented Programming,基于OOP延伸出来的编程思想。主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
每种编程思想都有各自的优点,它们适用在不同的情况下:面向过程性能很高,面向对象比较易于管理和维护,面向切面使软件变得更灵活。新的编程范式,并不一定完全各方面都优于旧的编程范式,它们只是在某一特定领域或特殊场景下有着独到的优势。编程范式只有适合不适合项目特性,没有绝对的好坏。
OPP和OOP
面向过程是最为实际的一种思考方式,就算是面向对象的方法也是含有面向过程的思想。可以说面向过程是一种基础的方法。它考虑的是实际地实现。一般的面向过程是从上往下步步求精,所以面向过程最重要的是模块化的思想方法。当程序规模不是很大时,面向过程的方法还会体现出一种优势。因为程序的流程很清楚,按着模块与函数的方法可以很好的组织。
面向对象是基于对象概念,以对象为中心,以类和继承为构造机制,来认识、理解、刻画客观世界和设计、构建相应的软件系统。类和继承是是适应人们一般思维方式的描述范式。方法是允许作用于该类对象上的各种操作。这种对象、类、消息和方法的程序设计范式的基本点在于对象的封装性和类的继承性。通过封装能将对象的定义和对象的实现分开,通过继承能体现类与类之间的关系,以及由此带来的动态联编
和实体的多态性。
举个栗子:
比如完成“吃饭”这个任务。
面向过程的写法,需要封装一个eat()函数:
如果是狗吃屎,则eat(狗,屎);
如果是人吃肉,则eat(人,肉);
eat是人和狗共用的吃饭本能。
那如果之后要处理猫吃鱼、鱼吃虾、奥特曼吃小怪兽呢?eat函数中就会存在大量的if…else的判断,这段代码,无疑是很恶心的。
如果是面向对象思想,如何来解决这个问题呢?
我们发现,狗、人、猫、鱼、奥特曼,都有一个“吃”的共性。我们抽象出每个受体的类,然后继承,这样都具有“吃”的方法。
当我们想要执行狗吃屎时,那就“狗->eat(屎)”,这样,我们从面向过程维护eat()的焦点,转移到了面向对象维护角色的焦点上来。我们只需要维护好不同的角色(类)就好了,并且狗的eat不会影响到猫的eat,猫的eat也不会影响到人的eat。
所以,oop思想非常贴近软件工程高内聚的思想:自己管好自己的东西,自己做好自己的事情。
大多数支持面向对象的语言,同时也支持面向过程,不论是JAVA、PHP,还是JS,它们都还无法完全面向对象,因为面向过程是必然的,面向过程代表着必要的程序流程,调动对象进行组合或对象内部能力的实现,都一定会存在“过程”,它最终还是需要通过拆分步骤来指导最具体的执行细节。
在此,我们也能得到一些感悟,许多事情并非完全非黑即白,非OOP就必然是OPP,特别是思想层面的东西,它们呈现出互相结合的形态,从OPP到OOP,这是一个思想进步的过程。
AOP
面向切面编程主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
那么,AOP如何体现?
这里可以联想一下laravel的中间件、javaweb的拦截器、vue的Decorator…它们都是AOP思想的实践。装饰器模式、代理模式,它们也是基于AOP思想的设计模式。
AOP思想,指导我们通过找到平整切面的形式,插入新的代码,使新插入的代码对切面上下原有流程的伤害降到最低。
举个栗子:
我们拿laravel中间件做什么?
权限、日志、请求过滤、请求频率限制、csrf过滤……我们知道,中间件对于controller的业务逻辑,不会有任何伤害。
如果没有这个切面,我们想要记录请求日志,可能需要在每个controller的具体方法中写日志记录的代码,或者调用日志记录的函数、方法。
这会使一段记录日志的代码,或调用记录日志的调用语句出现在许多controller中,这与controller原本要关注的逻辑无关,使controller职责不单一,提高维护成本。
当然,我们可能会写一个父类,让许多controller来继承这个父类,然后统一在父类的__construct方法中记录日志,以此来解决耦合问题。
但实际上,这个父类的construct方法,不正是一个切面吗?它在原有流程中截取了一个切面,在切面中植入代码,以达到承上启下的作用,并且不对上下文产生伤害。
从这个例子中,我们也能得出另外一个思考:AOP指导我们寻找切面,但找到合适的切面,也尤为重要。就像上文,父类构造函数的切面和中间件的切面比起来,显然中间件这个切面更利于维护,你可以灵活选择中间件,但你无法灵活选择父类,因为决定你的controller继承什么父类的,不是切面中的代码,而是controller本身处理什么逻辑。
许多项目,OPP、OOP、AOP是同时存在的,它们是编程范式,是一种指导编程的思想,并非不能互相配合。
本文从宏观角度讲解三种编程思想的异同点,旨在引领大家打个样。具体到每个编程思想,其实还有很多深究的点,比如AOP思想,贯穿在很多设计模式中,比如代理模式、装饰器模式、职责链模式……;
1. 类和对象 ( Class & Object )
1.1 类
1.1.1 概述
- Java语言最基本单位就是类,类似于类型。
- 类是对某一类事物的抽象。
- 可以理解为模板或者设计图纸。
1.1.2 类的访问机制
- 在一个类中的访问机制:类中的方法可以直接访问类中的成员变量。
(例外:static方法访问非static,编译不通过。) - 在不同类中的访问机制:先创建要访问类的对象,再用对象访问类中定义的成员。
1.1.3 普通类的开发原则
1)一个java文件中可以创建多个class,但是被public修饰的class只能有一个,而且这个公共类必须与文件名一致,也就是说公共类的名字就是文件名;
2)类名必须可以明确地表示出一类的定义,如Person、Emp、Dept;
3)类中所有属性必须使用private进行封装,如private String name;
4)类中的所有属性都必须定义相应的setter()、getter()方法;
5)类中可以提供构造方法,为属性初始化,但不管提供了多少构造方法,一定要保留一个无参构造;
6)类中不允许直接使用System.ou.println()输出,所有内容要返回给被调用处输出。
7) 同一个包下,不允许创建相同类名的类,不同包下可以创建。
8)对于class的权限修饰只可以用public和default(缺省)。
- public类可以在任意地方被访问。
- default类只可以被同一个包内部的类访问。
1.2 对象
1.2.1 概述
每个对象具有三个特点:对象的状态,对象的行为和对象的标识。
- 对象的状态用来描述对象的基本特征。
- 对象的行为用来描述对象的功能。
- 对象的标识是指对象在内存中都有一个唯一的地址值用来和其他对象区分开来。
1.2.2 对象的创建和使用
- 创建对象语法:类名 对象名 = new 类名();
- 使用“对象名.对象成员”的方式访问对象成员(包括属性和方法)
1.2.3 创建对象的流程
Person p = new Person();//短短这行代码发生了很多事情
- 把Person.class文件加载进内存
- 在栈内存中,开辟空间,存放引用变量p
- 在堆内存中,开辟空间,存放Person对象
- 对成员变量进行默认的初始化
- 对成员变量进行显示初始化
- 执行构造方法(如果有构造代码块,就先执行构造代码块再执行构造方法)
- 堆内存完成
- 把堆内存的地址值赋值给变量p ,p就是一个引用变量,引用了Person对象的地址值
1.2.4 匿名对象
没有名字的对象,是对象的简化表示形式。
由于匿名对象没有对应的栈内存指向,所以只能使用一次,一次之后就会成为垃圾,并且等待被GC回收释放。
使用场景:当被调用的对象只调用一次时。(多次会创建多个对象浪费内存)
Demo d = new Demo();
d.sleep();
d.game();//这个d就是对象的名字。
匿名对象的写法:
new Demo().show();//创建了一个对象调方法
new Demo().game();//又创建了一个对象调方法
1.3 类和对象的关系
类是对某一类事物的抽象描述,对象是具体实现该事物的个体。(对象是可以明月使用的,而类不能,类是用来定义概念的)
1)计算机语言来怎么描述现实世界中的事物的?
- 属性 + 行为(属性 = Field = 成员变量 = 域、字段,行为 = Method = 方法 = 函数)
2)那怎么通过java语言来描述呢?
- 我们可以通过类来描述一类事物,用成员变量描述事物的属性,用方法描述事物的行为。
1.4 JVM内存结构
我们使用JVM中类的加载器和解释器对生成的字节码文件(.class)进行解释运行。意味着,需要将字节码文件对应的类加载到内存中,此时涉及到内存解析。
在类与对象方面:
- 虚拟机栈(Stack):即为平时提到的栈结构,我们将局部变量存储在栈结构中,保存堆内存的地址数值。
- 堆(Heap):我们将new出来的结构(比如:数组、对象)加载在堆空间中,保存对象具体的属性信息。(对象中非static的成员变量,加载在堆空间中。)
- 方法区:类的加载信息、常量池、静态域
- 虚拟机栈与程序计数器每个线程中都会存在一份,方法区和堆每个进程一份,供多个线程共享。
例如,创建Person类的实例对象代码如下:
Person p = new Person();
" Person p " :声明了一个 Person 类型(引用类型)的变量 p(对象名)。
" = " :将 new 出来 Person 对象在内存中的地址赋值给变量 p(引用类型的变量,只可能存储两类值:null 或 地址值(含变量类型)),p 变量便持有了对象的引用。
1.5 关于声明和定义的理解
声明是向编译器介绍名字——标识符。它告诉编译器“这个函数或变量在某处可找到,它的模样象什么”。
我们声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分配存储空间。
而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。
所谓定义就是(编译器)创建一个对象,为这个对象分配一块内存,并给它取上一个名字,这个名字就是就是我们经常所说的变量名或对象名。
无论定义的是函数还是变量,编译器都要为它们在定义点分配存储空间。对于变量,编译器确定变量的大小,然后在内存中开辟空间来保存其数据,对于函数,编译器会生成代码,这些代码最终也要占用一定的内存。
定义创建对象并为这个对象分配了内存,声明没有分配内存。
总之就是:把建立空间的声明成为“定义”,把不需要建立存储空间的成为“声明”。基本类型变量的声明和定义(初始化)是同时产生的;而对于对象来说,声明和定义是分开的。
2. 类成员之一 属性( Field )
2.1 概述
属性: 类的属性又称为成员变量
变量: 可以改变的数,称为变量。在Java语言中,所有的变量在使用前必须声明。
一般通过“变量类型 变量名 = 变量值 ;”这三部分来描述一个变量。如:int a = 3 ;
变量的使用原则:就近原则,即尽量控制变量的使用范围到最小
2.2 变量的分类
2.2.1 按照数据类型:
2.2.2 按照在类中声明的位置
2.3 成员变量和局部变量
- 在方法体外,类体内声明的变量称为成员变量。
- 在方法体内部声明的变量称为局部变量。
2.3.1 成员变量和局部变量的区别
注意:二者在初始化值方面的异同:
- 同:都有生命周期
- 异:成员变量有默认初始化值,而局部变量除形参外,均需显式初始化。
2.3.2 对象属性的默认初始化赋值
当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。除了基本数据类型之外的变量类型都是引用类型,引用类型(类、数组、接口)的初始化默认值是null。
3. 类成员之二 方法 ( Method )
3.1 概述
方法就是被命名的代码块,方法可以含参数可以不含参数,可以提高代码的复用性。
格式:
修饰符 返回值类型 方法名(参数类型 参数名){
执行语句
··········
return 返回值;
}
- 修饰符:方法的修饰符比较多,有对访问权限限定的,有静态修饰符 static ,最终修饰符final等。
- 返回值类型:用于限定方法返回值的数据类型
- 参数名(形参):是一个变量,用于接收调用方法时传入的数据。
- return:用于结束方法以及返回方法指定类型的值。
- 返回值:被return语句返回的值,该值会返回给调用者。
Tips:
1)如果方法不需要接收任何参数,则参数列表为空,即()内不写任何内容。
2)方法的返回值必须为方法声明的返回值类型,如果方法中没有返回值,返回值类型要声明为void,此时,方法中return语句可以省略。
3.2 方法的形参的传递机制
针对于方法的参数概念
- 形参:方法定义时,声明的小括号内的参数
- 实参:方法调用时,实际传递给形参的数据
Java的实参值如何传入方法呢?
- Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而实际参数本身不受影响。
规则:
- 如果参数是基本数据类型,此时实参赋给形参的是实参真实存储的数据值。
- 如果参数是引用数据类型,此时实参赋给形参的是实参存储数据的地址值。
推广:
- 如果变量是基本数据类型,此时赋值的是变量所保存的数据值。
- 如果变量是引用数据类型,此时赋值的是变量所保存的数据的地址值。
public class ValueTransferTest {
public static void main(String[] args) {
String s1 = "hello";
ValueTransferTest test = new ValueTransferTest();
/*传递的是String类型的变量,所以传递的是存储"hello"这个字符串内存的地址值*/
test.change(s1);
System.out.println(s1);//hi~~
}
public void change(String s){
/*s一开始存放的是"hello"的地址值,现在存放的是"hi~~"的地址值*/
s = "hi~~";
}
}
3.3 方法的重载 ( Overload )
方法的重载是指在同一个类中可以定义多个同名的方法,但是每个方法的参数列表不同(也就是指参数的个数和类型不同),程序在调用方法时,可以通过传递给他们的不同个数和类型的参数来决定具体调用哪个方法。
3. 4 方法的递归
方法的递归是指在一个方法的内部调用自身的过程,递归必须要有结束条件,不然就会陷入无限递归的状态,永远无法结束调用。
4. 封装 ( Encapsulation )
4.1 概述
我们程序设计追求“高内聚,低耦合”。
- 高内聚 :类的内部数据操作细节自己完成,不允许外部干涉;
- 低耦合 :仅对外暴露少量的方法用于使用。
隐藏对象内部的复杂性,只对外公开简单的接口。便于外界调用,从而提高系统的可扩展性、可维护性。通俗的说,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。
封装就是隐藏对象的属性和实现细节,仅仅对外提供公共的访问方式,比如类和方法。
好处:
- 提高安全性
- 提高重用性
4.2 private 关键字
是一个权限修饰符 ,可以用来修饰成员变量和成员方法,被私有化的成员只能在本类中访问。
1)如何封装?封装后的资源如何访问?
- 答:我们可以使用private关键字来封装成员变量与方法
2)如何访问私有资源?
- 关于成员变量:
setXxx – 对外提供公共的设置值方式
getXxx – 对外提供公共的获取值方式- 关于成员方法:
把私有方法放在公共方法里供外界调用即可
4.3 访问权限修饰符
Java权限修饰符public、protected、default (缺省,此修饰符不用写,写了会报错) 、private置于类的成员定义前,用来限定对象对该类成员的访问权限。(4种权限都可以用来修饰类的内部结构:属性、方法、构造器、内部类)
对于class的权限修饰只可以用public和default(缺省)。
- public类可以在任意地方被访问。
- default类只可以被同一个包内部的类访问。
5. 类成员之三 构造器 ( Constructor 构造方法 )
5.1 概述
- 用途:如果需要在实例化对象的同时就为这个对象的属性进行赋值,可以通过构造方法来实现。
1)创建对象
2)初始化对象信息
Tips:
构造器可以叫构造方法,但请注意,它不是一种方法,是一种独立的结构。首先构造器与方法的编写结构不同,其次执行的功能不同,构造器是用来创建对象使用的,而方法是一个功能的封装,是有了对象之后调用使用的。在类中构造器与方法是并列结构。
- 格式:
1)方法名与类同名
2)在方法名前面没有返回值类型的声明
3)在方法中不能使用return语句返回一个值
- 普通方法的格式: 修饰符 返回值类型 方法名(参数列表){方法体}
- 构造方法的格式: 修饰符 与类名同名的方法名(参数列表){方法体}
- 构造方法是一个用来创建对象的方法,只要创建对象,就会触发指定的构造方法,创建几个,触发几次。
- 构造方法执行的时机:创建对象时立即触发。
- Method method = new Method(); //Method()是一个无参构造方法
5.2 构造方法的重载
- 无参构造——没有参数的构造方法。
- 含参构造——含有参数的构造方法。
- 全参构造——构造方法的参数类型与类中属性完全一致的构造方法。
- 如果没有显示的定义类的构造器的话,系统会在类中默认提供无参构造器。
- 一旦添加了含参构造,默认的无参构造会被覆盖,所以要手动添加无参构造。
- 全参构造除了可以帮我们创建对象以外,还可以给这个对象的所有属性赋值。
5.3 this 关键字
5.3.1 this 是什么
在Java中,this关键字比较难理解,它的作用和其词义很接近,简单的说,this代表本类对象的一个引用对象,表示当前对象。
- 它在方法内部使用,即这个方法所属对象的引用。
- 它在构造器内部使用,表示该构造器正在初始化的对象。
- this 可以调用类的属性、方法和构造器。
5.3.2 this关键字的使用
在类的方法中,我们可以使用"this.属性"或"this.方法"的方式,调用当前对象属性或方法。但是,通常情况下,我们都择省略"this."。特殊情况下,如果方法的形参和类的属性同名时,我们必须显式的使用"this.变量"的方式,表明此变量是属性,而非形参。
1) 通过this关键字可以明确地去访问一个成员变量,解决与局部变量名称冲突问题。
class Person{
int age;
public person(int age){
this.age = age;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
在构造方法中如果使用" age “,访问的是局部变量,使用” this.age "则访问的是成员变量。
2)通过this关键字调用成员方法。
class person{
public void openMouth(){
.........
}
public void speak(){
this.openMouth();
}
}
在上面 speak() 方法中,使用 this 关键字调用 openMouth() 方法,调用成员方法的情况也可以省略 this. 不写,直接写 openMouth() 调用。
3)通过this调用构造方法,由于构造方法是在实例化对象时被Java虚拟机自动调用的,在程序中不能像调用其他方法一样去调用构造方法,但可以在一个构造方法中使用" this([参数1 , 参数2 ···]) " 的形式来调用其他构造方法。
public class Test {
public static void main(String[] args) {
//创建对象时会自动调用构造方法
Dog dog = new Dog("小旺旺");
}
}
class Dog{
public Dog() {//无参构造器
System.out.println("无参构造");
}
public Dog(String s) {//含参构造器
this();//在含参构造中 调用无参构造的功能
System.out.println("含参构造"+s);
}
}
我们在类的构造器中,可以显式的使用"this(形参列表)"方式,调用本类中指定的其他构造器。
- 规定:"this(形参列表)"必须声明在当前构造器的首行。
- 构造器中不能通过"this(形参列表)"方式调用自己。
- 如果一个类中有n个构造器,则最多有 n - 1构造器中使用了"this(形参列表)"
- 一个构造器内部,最多只能声明一个"this(形参列表)",用来调用其他的构造器。
6. 继承 ( Extends )
6.1 概述
extends:延展、扩展。继承是面向对象最显著的一个特征。
继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并扩展新的能力。
Java继承是会用已存在的类的定义作为基础建立新类的技术新类的定义可以增加新的数据或者新的功能,也可以使用父类的功能,但不能选择性的继承父类(超类/基类)。
这种继承使得复用以前的代码非常容易,能够大大的缩短开发的周期,降低开发费用。
为什么要有类的继承性?(继承性的好处)
- 减少了代码的冗余,提高了代码的复用性。
- 便于功能的扩展。
- 为之后多态性的使用,提供了前提。
6.2 继承的使用
1)我们通过关键字extends建立子类(派生类)与父类(超类)的继承关系,格式:
class A extends B { } (子类 extends 父类)
A : 子类、派生类、subclass
B : 父类、超类、基类、superclass
2)子类继承父类后,相当于子类把父类的功能复制了一份,子类中就获取了父类中声明的所有的属性和方法。
3)父类中声明为 private 的属性或方法,子类继承父类以后,仍然认为获取了父类中私的结构。只因为封装性的影响,使得子类不能直接调用父类的结构而已。
4)java只支持单继承,一个子类只能有一个父类,但一个父类可以有多个子类
5)继承具体传递性,爷爷的功能传给爸爸,爸爸的功能传给孙子。
6)子类继承父类以后,还可以声明自己特有的属性或方法:实现功能的拓展。
7)子类和父类的关系,不同于子集和集合的关系,依赖性非常强,强耦合。
6.3 方法的重写 ( Overriding )
在继承关系中,子类会自动继承父类中定义的方法,但有时在子类中需要对继承的方法进行一些修改,即对父类的方法进行重写。
6.3.1 方法重写的规则
约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法
1)子类中重写的方法需要和父类被重写的方法具有相同的方法名、参数列表和返回值类型。
2)子类重写父类方法时,不能使用比父类中被重写的方法更严格的访问权限,如父类中的方法是 public 的,子类的方法就不能是 private 的。
3)子类不能重写父类中声明为 private 权限的方法。(父类的私有方法不能被重写,子类如果有与父类私有方法同名的方法,看作子类自有的方法)
4)返回值类型:
- 父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void。
- 父类被重写的方法的返回值类型是A引用类型,则子类重写的方法的返回值类型可以是A类或A类的子类。
- 父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值类型必须是相同的基本数据类型(必须也是double)。
- 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型。
5)子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。
6.3.2 重载Overload 与重写Override的区别
1)二者的概念:
- 重载: 是指在一个类中,允许存在多个同名方法,而参数的类型与个数不同,是对同名方法的名称做修饰。
- 重写: 是指在继承关系类中,子类存在方法名称、参数的类型与个数、返回值类型与父类完全相同的方法。
2)多态性
- 重载: 不表现为多态性。对于编译器而言,这些同名方法就成为了不同的方法。它们的调用地址在编译期就绑定了。在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”。
- 重写: 表现为多态性。是指发生了继承关系以后(两个类),子类去修改父类原有的功能。而对于多态,只等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定”。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”
3)意义
- 重载: 是为了方便外界对方法进行调用,什么样的参数程序都可以找到对应的方法来执行,体现的是程序的灵活性。
- 重写: 是在不修改源码的前提下,进行功能的修改和拓展。(OCP原则:面向修改关闭,面向拓展开放)
6.4 super 关键字
6.4.1 super 是什么
当子类重写父类的方法后,子类对象将无法访问父类被重写的方法,为了解决这个问题,在Java中提供了 super 关键字用来访问父类的成员。可以理解为 super 代表的是父类的一个引用对象。
6.4.2 super 关键字的使用
1)super 关键字可以理解为:父类的
2)可以用来调用的结构:属性、方法、构造器
3)super代表的是父类的一个引用,你可以把它看成是Father super = new Father();。
4)super调用属性、方法:
- 我们可以在子类的方法或构造器中。通过使用" super.属性 " 或 " super.方法 " 的方式,显式的调用父类中声明的属性或方法。但是,通常情况下,我们习惯省略"super."
- 特殊情况:当子类和父类中定义了同名的属性时,我们要想在子类中调用父类中声明的属性,则必须显式的使用"super.属性"的方式,表明调用的是父类中声明的属性。
- 特殊情况:当子类重写了父类中的方法以后,我们想在子类的方法中调用父类中被重写的方法时,则必须显式的使用 " super.方法 " 的方式,表明调用的是父类中被重写的方法。
5)super调用构造器:
- 子类创建对象时,默认会先调用父类的构造方法,在子类中第一行默认存在super() , 表示调用父类的无参构造。当父类中没有无参构造时,需要通过 " super(带参数); " 调用其他的构造方法。
- 我们可以在子类的构造器中显式的使用 " super(形参列表) " 的方式,调用父类中声明的指定的构造器。
- " super(形参列表) " 的使用,必须声明在子类构造器的首行!
- 我们在类的构造器中,针对于" this(形参列表) " 或 " super(形参列表) "只能二选一,不能同时出现。
- 在构造器的首行,没显式的声明" this(形参列表) " 或 " super(形参列表) ",则默认调用的是父类中空参的构造器:super()。
- 在类的多个构造器中,至少一个类的构造器中使用了" super(形参列表) ",调用父类中的构造器。
- 构造方法不能被继承,因此不能被重写,只能被重载。
6.5 子类实例化全过程
1)从结果上看:继承性
-
子类继承父类以后,就获取了父类中声明的属性或方法。
-
创建子类的对象,在堆空间中,就会加载所父类中声明的属性。
2)从过程上看:
- 当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所的父类的结构,所以才可以看到内存中父类中的结构,子类对象才可以考虑进行调用。
Tips:
虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。
1)为什么super(…)或this(…)调用语句只能作为构造器中的第一句出现?
- 必须在构造器的第一行放置 super 或者 this 构造器,否则编译器会自动地放一个空参数的 super 构造器的,其他的构造器也可以调用 super 或者 this ,调用成一个递归构造链,最后的结果是父类的构造器(可能有多级父类构造器)始终在子类的构造器之前执行,递归的调用父类构造器。无法执行当前的类的构造器。也就不能实例化任何对象,这个类就成为一个无为类。
从另外一面说,子类是从父类继承而来,继承了父类的属性和方法,如果在子类中先不完成父类的成员的初始化,则子类无法使用,应为在 java 中不允许调用没初始化的成员。在构造器中是顺序执行的,也就是说必须在第一行进行父类的初始化。而 super() 能直接完成这个功能。 This() 通过调用本类中的其他构造器也能完成这个功能。
因此, this() 或者 super() 必须放在第一行。
2)为什么super(…)和this(…)调用语句不能同时在一个构造器中出现?
- 如果this()和super()都存在,那么就会出现:初始化父类两次的不安全操作,因为当super()和this()同时出现的时候,在调用完了super()之后 还会执行this(),而this()中又会自动调用super(),这就造成了调用两次super()的结果。如果你继承的父类没有无参数构造函数,那么你这个类第一句必须显示的调用super关键字,来调用父类对应的有参构造函数,否则不能通过编译。
7. 多态 ( Polymorphism )
7.1 概述
多态是面向对象程序设计(OOP)的一个重要特征,指同一个实体同时具有多种形式,即同一个对象,在不同时刻,代表的对象不一样,指的是对象的多种形态。
在Java中为了实现多态,允许使用一个父类类型的变量来引用一个子类类型的对象,根据被引用子类对象特征的不同,得到不同的运行结果。
7.2 多态的使用
1)多态使用的前提
- 需要存在继承或者实现关系
- 有方法的重写
2)多态的体现 —— 父类与子类之间的转换操作,可以直接应用在抽象类和接口上。
- 向上转型:子类对象变为父类对象,父类 父类对象 = 子类实例; 自动。
- 向下转型: 父类对象变成之类对象,子类 子类对象 = (子类)父类实例; 强制。
以下所说的都是向上转型的特点:
3) Java引用变量有两个类型:编译时类型和运行时类型,编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定,若编译时类型和运行时类型不一致,就出现了对象的多态性。
- 编译时:要查看引用变量所声明的类中是否有所调用的方法。
- 运行时:调用实际new的对象所属的类中的重写方法。
简称:即多态情况,编译时,看左边;运行时,看右边。
-
成员方法:
“看左边”:看的是父类的引用(父类中不具备子类特有的方法)
“看右边”:看的是子类的对象(实际运行的是子类重写父类的方法) -
成员变量:不具备多态性,只看引用变量所声明的类。(对象的多态性只适用于方法,不适用于属性。所以,在多态中成员变量都是使用的父类的。)
多态性的使用举例:
/* 对于多态,只有等到方法调用的那一刻,解释运行器才会确定所要调用的具体方法
这称为“晚绑定”或“动态绑定”。*/
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test = new AnimalTest();
test.func(new Dog());//狗吃骨头 汪!汪!汪!
test.func(new Cat());//猫吃鱼 喵!喵!喵!
}
//创建调取子类对象的方法
public void func(Animal animal){//Animal animal = new xxx();
animal.eat();
animal.shout();
}
}
//创建一个Animal类
class Animal{
public void eat(){
System.out.println("动物:进食");
}
public void shout(){
System.out.println("动物:叫");
}
}
//创建一个Dog类 继承 Animal类
class Dog extends Animal{
public void eat(){
System.out.println("狗吃骨头");
}
public void shout(){
System.out.println("汪!汪!汪!");
}
}
//创建一个Cat类 继承 Animal类
class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public void shout(){
System.out.println("喵!喵!喵!");
}
}
多态是编译时行为还是运行时行为?是运行时行为,动态绑定。再次证明:
class Animal {
protected void eat() {
System.out.println("animal eat food");
}
}
class Cat extends Animal {
protected void eat() {
System.out.println("cat eat fish");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog eat bone");
}
}
class Sheep extends Animal {
public void eat() {
System.out.println("Sheep eat grass");
}
}
public class InterviewTest {
public static Animal getInstance(int key) {
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}
public static void main(String[] args) {
//随机获取 0 1 2
int key = new Random().nextInt(3);
System.out.println(key);
//根据获取的值返回子类对象
Animal animal = getInstance(key);
animal.eat();
}
}
Tips: 虚拟方法调用(多态情况下)
子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法
编译时类型和运行时类型
编译时e为Person类型,而方法的调用是在运行时确定的,所以调用的是Student类
的getInfo()方法。这种情况称为动态绑定
4)多态作用:可以把不同的子类对象都当作父类来看,进而屏蔽不同子类对象之间的差异,写出通用的代码,做出通用的编程,统一调用标准。(提高了代码的通用性,常称作接口重用)
以下是向下转型的特点:
有了对象的多态性以后,内存中实际上是加载了子类特有的属性和方法的,但是由于变量声明为父类类型,导致编译时,只能调用父类中声明的属性和方法。子类特有的属性和方法不能调用。此时使用向下转型能够调用子类特的属性和方法。
Person p = new man;//将Man类向上转型
Man m = (Man) p;//可以向下转型
Woman wm = (Woman) p;//报错,ClassCastException的异常
7.3 instanceof 关键字
使用强转时,可能出现ClassCastException的异常。为了避免在向下转型时出现ClassCastException的异常,我们在向下转型之前,先进行instanceof的判断,一旦返回true,就进行向下转型。如果返回false,不进行向下转型。
- a instanceof A:判断对象a是否是类A的实例。如果是,返回true,如果不是,返回false。
- 当类B是类A的父类或超父类时。如果 a instanceof A返回true,则 a instanceof B也返回true。
- 要求a所属的类与类A必须是子类和父类的关系,否则编译错误。
//先进行判断,再向下转型:
//1.
Class Man extends Person;
//编译通过,运行通过
Person p = new man;
if(p instanceof Man){
Man m = (Man)p;
}
/****************************************/
//2.
Class Teacher,Student extends Person;
//编译通过,运行报错
Person t = new Teacher();
if(t instanceof Teacher){
Student s = (Student) t;
}
8. static 关键字
8.1 概述
static是一个关键字,表示静态,用于修饰类的成员,如成员变量、成员方法以及代码块等,被修饰的资源就是静态资源。
可以用来修饰的结构:属性、方法、代码块、内部类。(主要用来修饰类的内部结构)
- 静态资源的加载优先于对象的创建,随着类的加载而加,且只加载一次,并且一直存在,直到类消失,它才会消失
- 被静态修饰的资源可以通过 " 类.静态资源 " 的方式直接进行调用
- 由于类只会加载一次,则静态变量在内存中也只会存在一份:存在方法区的静态域中。
8.2 static 的使用
1)static修饰属性:静态变量(或类变量)
属性,是否使用static修饰,又分为:静态属性 vs 非静态属性(实例变量)
-
实例变量:我们创建了类的多个对象,每个对象都独立的拥一套类中的非静态属性。当修改其中一个对象中的非静态属性时,不会导致其他对象中同样的属性值的修改。
-
静态变量:我们创建了类的多个对象,多个对象共享同一个静态变量。当通过某一个对象修改静态变量时,会导致其他对象调用此静态变量时,是修改过了的。
静态变量和实例变量的区别是什么?
- 1.在语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加。
- 2.在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。
静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。
总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
2)static修饰方法:静态方法、类方法
- 随着类的加载而加载,可以通过 " 类.静态方法 " 的方式进行调用。
- 静态的调用关系:
- 静态方法中,只能调用静态的方法或属性。(静态资源只能调用静态资源)
- 非静态方法中,既可以调用非静态的方法或属性,也可以调用静态的方法或属性。(非静态资源既可以调用静态资源,也调用非静态资源)
3)如何判定属性和方法应该使用static关键字:
-
关于属性
属性是可以被多个对象所共享的,不会随着对象的不同而不同的。
类中的常量也常常声明为static -
关于方法
操作静态属性的方法,通常设置为static的
工具类中的方法,习惯上声明为static的。 比如:Math、Arrays、Collections
Tips:
- 静态资源属于优先加载的类资源,静态方法不存在重写的现象。谁的对象来调用,那就使用哪个类的静态方法,多态对象调用的静态方法是父类的。
- 多态对象使用时,如果父子类中出现同名静态方法,使用的还是父类的。
- 在静态的方法内,不能使用this关键字、super关键字。
- 关于静态属性和静态方法的使用,大家都从生命周期的角度去理解。
8.3 理解main方法的语法
由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。
又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。
- main()方法作为程序的入口
- main()方法也是一个普通的静态方法
- main()方法可以作为我们与控制台交互的方式。(之前:使用Scanner)
如何将控制台获取的数据传给形参:String[] args?
- 运行时:java 类名 “Tom” “Jerry” “123” “true”
sysout(args[0]);//“Tom”
sysout(args[3]);//“true” -->Boolean.parseBoolean(args[3]);
sysout(args[4]);//报异常
9. 类成员之四 代码块(初始化块)
静态代码块:用static 修饰的代码块
- 可以有输出语句。
- 可以对类的属性、类的声明进行初始化操作。
- 不可以对非静态的属性初始化。即:不可以调用非静态的属性和方法。
- 若有多个静态的代码块,那么按照从上到下的顺序依次执行。
- 静态代码块的执行要先于非静态代码块。
- 静态代码块随着类的加载而加载,且只执行一次。
非静态代码块:没有static修饰的代码块
- 可以有输出语句。
- 可以对类的属性、类的声明进行初始化操作。
- 除了调用非静态的结构外,还可以调用静态的变量或方法。
- 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
- 每次创建对象的时候,都会执行一次。且先于构造器执行。
静态代码块
位置:类里方法外;格式:static{ }
执行时机:随着类的加载而加载,优先与对象加载,并且只加载一次
作用:用于加载一些需要第一时间就加载并且只加载一次的资源,通常用于对类的成员初始化。
构造代码块:
位置:类里方法外,与成员变量一样;格式:{ }
执行时机:创建对象时执行,并且先于构造方法执行
作用:用于提取所有构造方法的共性内容
局部代码块:
位置:方法里;格式:{ }
执行时机:调用此局部代码块所在的方法时才会执行
作用:用于控制变量的作用范围,范围越小越好
Tips:
执行顺序:
静态代码块->构造代码块->构造方法【创建好对象】->调用局部代码块所处的普通方法->执行局部代码块
为什么是这样的顺序呢?
- 静态代码块它也是静态资源,静态资源随着类的加载而加载,优先于对象的创建
构造代码块也是创建对象时调用,不创建对象不会执行
局部代码块不是一定会执行,必须调用的方法里有局部代码块,局部代码块才会执行
10. final 关键字
final是一个关键字,可以用来修饰类、变量和方法,表示最终、无法改变的。
- 被final修饰的类是最终类,不可以被继承。提高安全性,提高程序的可读性。( 比如String类、System类、StringBuffer类等 )
- 被final修饰的方法是方法的最终实现,不可以被重写。( 比如Object类中的getClass(); )
- 被final修饰的变量(成员变量和局部变量)是常量,只能赋值一次,且不可以被修改。
- static final 用来修饰属性:全局常量。
Tips:
声明常量时,名称大写,且必须给常量赋值,不赋值会报错(比如 final double MY_PI = 3.14;)
final修饰属性:可以考虑赋值的位置有:显式初始化、代码块中初始化、构造器中初始化。不能在方法中赋值,因为这些结构都会对象加载之前执行。
public class FinalTest {
final int WIDTH = 0;
final int LEFT;
final int RIGHT;
//构造代码块
{
LEFT = 1;
}
//无参构造
public FinalTest(){
RIGHT = 2;
}
//含参构造
public FinalTest(int n){
RIGHT = n;
}
}
尤其是使用final修饰形参时,表明此形参是一个常量。当我们调用此方法时,给常量形参赋一个实参。一旦赋值以后,就只能在方法体内使用此形参,但不能进行重新赋值。
public class FinalTest {
public void show(final int num){
num = 20; //错误,只能对num传来的实参值进行使用,不能再赋值,编译不通过
System.out.println(num);
}
public static void main(String[] args) {
test.show(10);
}
}
11. 抽象类和接口
11.1 abstract 关键字
11.1.1 抽象类的定义
当定义一个类时,常常需要定义一些方法来描述该类的行为特征,但有时这些方法的实现是无法确定的。
比如,在定义Animal类时,shout()方法用于表示动物的叫声,但是针对不同的动物,叫声也是不同的,因此在shout()方法中无法准确描述动物的叫声。
因此,Java允许在定义方法时不写方法体,不包含方法体的方法为抽象方法,抽象方法必须使用 abstract 关键字来修饰,abstract 用于类、方法中。
abstract void method();//定义抽象方法method()
抽象方法:只有方法的声明,没有方法的实现。以 ; 结束。
当一个类中包含了抽象方法,该类必须使用 abstract 关键字来修饰,使用 abstract 关键字修饰的类为抽象类。抽象类是用来被继承的,不能被实例化。
//定义抽象类 Anima()
abstract class Anima{
//定义抽象方法 shout()
abstract String shout();
}
包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法的。
若子类重写了父类中的所的抽象方法,并提供方法体后,此子类方可实例化。
若子类没重写父类中的所的抽象方法,则此子类也是一个抽象类,需要使用abstract修饰。
11.1.2 抽象类的特性
抽象的使用前提:继承性。因为抽象需要靠继承才能实现,所以:
- 不能用abstract修饰私有方法、静态方法( 因为私有方法,静态方法不能被继承。)、final的方法、final的类(因为抽象类必须要有子类,而final不能有子类。)。
- 不能用abstract修饰变量、代码块、构造器。
- 抽象类中一定有构造器,便于子类实例化时调用(涉及:子类对象实例化的全过程)开发中,都会提供抽象类的子类,让子类对象实例化,完成相关的操作。
如果一类中都是普通方法,那它为啥还要被修饰成抽象类呢?
- 因为抽象类不可以被实例化,所以如果不想让外界创建本类的对象,就可以把普通类声明成抽象类。
11.2 interface 关键字
11.2.1 接口的定义
如果一个类(或抽象类中)中的所有方法没有方法体(或所有的方法都是抽象方法),则可以将这个类用另外一种方式来定义,即接口。在定义接口时,需要使用interface关键字来声明。如下:
public interface Runner {
int ID = 1;
void start();
public void run();
void stop();
}
//相当于
public interface Runner {
public static final int ID = 1;
public abstract void start();
public abstract void run();
public abstract void stop();
}
- 接口(interface)是抽象方法和常量值定义的集合。
- 接口使用用interface来定义。
- 接口中的所有成员变量都默认是由public static final修饰的。
- 接口中的所有抽象方法都默认是由public abstract修饰的。
- 接口中不能定义构造器(意味着接口不能被实例化)。
- 接口采用多继承机制。
- 接口突破了java单继承的局限性
- 接口和类之间可以多实现,接口与接口之间可以多继承
- 接口是对外暴露的规则,是一套开发规范
- 接口提高了程序的功能拓展,降低了耦合性
11.2.2 关于接口的理解
一方面,有时必须从几个类中派生出一个子类,继承它们所有的属性和方法。但是,Java不支持多重继承。有了接口,就可以得到多重继承的效果。
另一方面,有时必须从几个类中抽取出一些共同的行为特征,而它们之间又没有is-a的关系,仅仅是具有相同的行为特征而已。例如:鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等都支持USB连接。
接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个"是不是"的关系,而接口实现则是 "能不能"的关系。
接口的本质是契约,标准,规范,就像我们的法律一样。制定好后大家都
要遵守。
接口理解与实现的举例:
/*
* 接口的使用
* 1.接口使用上也满足多态性
* 2.接口,实际上就是定义了一种规范
* 3.开发中,体会面向接口编程!
*/
public class USBTest {
public static void main(String[] args) {
Computer computer = new Computer();
//1.创建了接口的非匿名实现类的非匿名对象
Flash flash = new Flash();
computer.transferData(flash);
//2. 创建了接口的非匿名实现类的匿名对象
computer.transferData(new Printer());
//3. 创建了接口的匿名实现类的非匿名对象
USB phone = new USB(){
@Override
public void start() {
System.out.println("手机开始工作");
}
@Override
public void stop() {
System.out.println("手机结束工作");
}
};
computer.transferData(phone);
//4. 创建了接口的匿名实现类的匿名对象,只能使用一次
com.transferData(new USB(){
@Override
public void start() {
System.out.println("mp3开始工作");
}
@Override
public void stop() {
System.out.println("mp3结束工作");
}
});
}
}
class Computer{
public void transferData(USB usb){//USB usb = new Flash();/new Printer ();
usb.start();
System.out.println("具体传输数据的细节");
usb.stop();
}
}
//定义一次 USB 接口
interface USB{
//常量:定义了长、宽、最大最小的传输速度等
void start();
void stop();
}
//定义一个 U盘类 实现 USB 接口
class Flash implements USB{
@Override
public void start() {
System.out.println("U盘开启工作");
}
@Override
public void stop() {
System.out.println("U盘结束工作");
}
}
//定义一个打印机类 实现 USB 接口
class Printer implements USB{
@Override
public void start() {
System.out.println("打印机开启工作");
}
@Override
public void stop() {
System.out.println("打印机结束工作");
}
}
11.2.3 接口的特性
1)通过implements让子类来实现接口:先写extends,后写implements
class SubClass extends SuperClass implements InterfaceA{ }
2)实现接口的类中必须提供接口中所有方法的具体实现内容,方可实例化。否则,仍为抽象类。
public class InterfaceTest {
public static void main(String[] args) {
System.out.println(Flyable.MAX_SPEED);
System.out.println(Flyable.MIN_SPEED);
Plane plane = new Plane();//实例化 Plane类
plane.fly();//调用 Plane类 方法
plane.stop();//调用 Plane类 方法
}
}
//定义Flyable接口
interface Flyable{
//全局常量,省略了public static final
public static final int MAX_SPEED = 7900;//第一宇宙速度
int MIN_SPEED = 1;
//抽象方法,省略了public abstract
void fly();
void stop();
}
//创 Plane类并实现Flyable中所有方法,可以被实例化
class Plane implements Flyable{
@Override
public void fly() {
System.out.println("通过引擎起飞");
}
@Override
public void stop() {
System.out.println("驾驶员减速停止");
}
}
//创建Kite类,没有实现Flyable接口全部方法,仍为抽象类,需要使用abstract关键字修饰,否则编译报错
abstract class Kite implements Flyable{
@Override
public void fly() {
}
}
3)一个类可以实现多个接口,接口也可以继承其它接口,弥补了Java单继承性的局限性。
interface Flyable{
public static final int MAX_SPEED = 7900;
int MIN_SPEED = 1;
void fly();
void stop();
}
interface Attackable{
void attack();
}
interface AA{
void method1();
}
interface BB{
void method2();
}
//接口也可以继承其它接口,而且可以多继承
interface CC extends AA,BB{
}
abstract class Kite implements Flyable{
}
//一个类可以实现多个接口
class Bullet extends Object implements Flyable,Attackable,CC{
@Override
public void attack() {
// TODO Auto-generated method stub
}
@Override
public void fly() {
// TODO Auto-generated method stub
}
@Override
public void stop() {
// TODO Auto-generated method stub
}
@Override
public void method1() {
// TODO Auto-generated method stub
}
@Override
public void method2() {
// TODO Auto-generated method stub
}
}
4)接口的主要用途就是被实现类实现。(面向接口编程)
5)与继承关系类似,接口与实现类之间存在多态性。
6)接口和类是并列关系,或者可以理解为一种特殊的类,(但不是类!)。类描述的是一类事物的属性和方法,接口则是包含实现类要实现的方法。从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义。(JDK7.0及之前),而没有变量和方法的实现。
11.2.4 Java 8 接口新特性的应用
Java 8中,你可以为接口添加静态方法和默认方法。从技术角度来说,这是完全合法的,只是它看起来违反了接口作为一个抽象定义的理念。
静态方法:使用 static 关键字修饰。可以通过接口直接调用静态方法,并执行其方法体。我们经常在相互一起使用的类中使用静态方法。你可以在标准库中找到Collection/Collections或者Path/Paths这样成对的接口和类。
默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。我们在已有的接口中提供新方法的同时,还保持了与旧版本代码的兼容性。比如:java 8 API中对Collection、List、Comparator等接口提供了丰富的默认方法。
public class SubClassTest {
public static void main(String[] args) {
SubClass s = new SubClass();
//知识点1:接口中定义的静态方法,只能通过接口来调用。
// s.method1(); //编译不通过
// SubClass.method1();//编译不通过
//知识点2:通过实现类的对象,可以调用接口中的默认方法。
//如果实现类重写了接口中的默认方法,调用时,仍然调用的是重写以后的方法
CompareA.method1();
//知识点3:如果子类(或实现类)继承的父类和实现的接口中声明了同名同参数的默认方法,那么子类在没有重写此方法的情况下,默认调用的是父类中的同名同参数的方法。-->类优先原则
//知识点4:如果实现类实现了多个接口,而这多个接口中定义了同名同参数的默认方法,那么在实现类没有重写此方法的情况下,报错。-->接口冲突。这就需要我们必须在实现类中重写此方法。
s.method2();
s.method3();
}
}
public interface CompareA {
//静态方法
public static void method1(){
System.out.println("CompareA:北京");
}
//默认方法
public default void method2(){
System.out.println("CompareA:上海");
}
default void method3(){
System.out.println("CompareA:上海");
}
}
public interface CompareB {
default void method3(){
System.out.println("CompareB:上海");
}
}
class SubClass extends SuperClass implements CompareA,CompareB{
public void method2(){
System.out.println("SubClass:上海");
}
public void method3(){
System.out.println("SubClass:深圳");
}
//知识点5:如何在子类(或实现类)的方法中调用父类、接口中被重写的方法
public void myMethod(){
method3();//调用自己定义的重写的方法
super.method3();//调用的是父类中声明的
CompareA.super.method3(); //调用接口中的默认方法
CompareB.super.method3();
}
}
接口中的静态方法
- 接口中定义的静态方法,只能通过接口来调用。接口名.staticMethod();
接口中的默认方法
- 通过实现类的对象,可以调用接口中的默认方法。实现类对象名.defaultMethod();
- 若一个接口中定义了一个默认方法,而另外一个接口中也定义了一个同名同参数的方法(不管此方法是否是默认方法),在实现类同时实现了这两个接口时,会出现 —— 接口冲突。若两个接口的同名方法都是抽象方法,在实现类同时实现了这两个接口时,会认为将两个抽象方法同时实现。
解决办法:实现类必须覆盖接口中同名同参数的方法,来解决冲突。- 若一个接口中定义了一个默认方法,而父类中也定义了一个同名同参数的非抽象方法,则不会出现冲突问题。因为此时遵守:类优先原则。接口中具有相同名称和参数的默认方法会被忽略。
在子类(或实现类)的方法中调用接口中被重写的方法
- 接口名.super.method();
11.2.5 总结
面试题:子类的接口中与父类中的属性名相同,怎么调用?
interface A {
int x = 0;//全局常量
}
class B {
int x = 1;//成员变量
}
class C extends B implements A {
public void pX() {
System.out.println(x);//编译不通过。因为x是不明确的
System.out.println(super.x);//1
System.out.println(A.x);//0
}
public static void main(String[] args) {
new C().pX();
}
}
Tips:
1) 类与类的关系
- 继承关系,只支持单继承。
比如,A是子类 B是父类,A具备B所有的功能。(除了父类的私有资源和构造方法)
子类如果要修改原有功能,需要重写。(方法签名与父类一致 + 权限修饰符 >= 父类修饰符)
2) 类和接口的关系
- 实现关系,可以单实现,也可以多实现。 class A implements B,C{}
其中A是实现类,B和C是接口,A拥有BC接口的所有功能,只是需要进行全部方法的重写,否则A就是抽象类。
3)接口与接口的关系
- 继承关系,可以单继承,也可以多继承 。interface A extends B,C{}
其中ABC都是接口,A是子接口,具有BC接口的所有功能(抽象方法)
class X implements A{}
X实现类需要重写ABC接口的所有方法,否则就是抽象类
class A extends B implements C,D{}
其中A是实现类,也是B的子类,同时拥有CD接口的所有功能
这时A需要重写CD接口里的所有抽象方法
1)抽象类与接口的区别
- 抽象类是一个特殊的类,抽象类中可以包含没有方法体的方法。(抽象方法)。
- 接口可以理解成一个特殊的抽象类,接口里的都是抽象方法,没有普通方法。
- 接口会为方法自动拼接public abstract,还会为变量自动拼接public final static。
- 抽象类可以有构造方法,用来给子类创建对象,接口中没有构造方法
抽象类和接口都不能实例化(创建对象)。- 接口可继承接口,并可多继承接口,但类只能单继承。
- 抽象方法只能声明,不能实现,接口是设计的结果 ,抽象类是重构的结果。
12. 类成员之五 内部类
12.1 概述
如果一个类存在的意义就是为指定的另一个类,可以把这个类放入另一个类的内部。在Java中,允许一个类的定义位于另一个类的内部,前者称为内部类,后者称为外部类。
当一个事物的内部,还有一个部分需要一个完整的结构进行描述,而这个内部的完整的结构又只为外部事物提供服务,那么整个内部的完整结构最好使用内部类。
A类中又定义了B类,B类就是内部类,B类可以当做A类的一个成员看待:
class A {//我是外部类 A
class B{//我是内部类
}
}
内部类B只为A类服务,可以看成外部类的一个特殊成员
分类:
- 成员内部类(static 成员内部类 和 非 static 成员内部类)
- 局部内部类 (方法内、代码块内、构造器内)
- 匿名内部类
内部类的特点
- 内部类的名字不能与包含它的外部类类名相同。
- 内部类可以直接访问外部类中的成员,包括私有成员。
- 外部类要访问内部类的成员,必须要建立内部类的对象。
- 在成员位置的内部类是成员内部类。
- 在局部位置的内部类是局部内部类。
12.2 成员内部类
1)成员内部类作为类的成员的角色:
成员内部类可以直接使用外部类的所有成员,包括私有的数据。
- 和外部类不同,Inner class还可以声明为private或protected。
- 可以声明为abstract类 ,因此可以被其它的内部类继承。
- 可以声明为final的
- Inner class 可以声明为static ,但此时就不能再使用外层类的非static的成员变量。静态资源访问时不需要创建对象,可以通过类名直接访问。(>成员内部类可以直接使用外部类的所有成员,包括私有的数据。)
- 在静态内部类中只能访问外部类的静态成员,访问非静态成员编译会报错。
- 在静态内部类中可以定义静态的成员,而在非静态的内部类中不允许定义静态的成员,否则编译不通过。
- 外部类访问静态类中的静态资源可以直接通过”. . . ”链式加载的方式访问。
class Outer{
static int num1;
int num2;
class Inner1{
static int num = 10;//编译不通过,非静态的内部类中不允许定义静态的成员
}
static class Inner2{
num1 = 10;//给静态成员变量num1赋值,编译通过
Outer.this.num2 = 10;//给普通成员变量赋值,编译不通过
static int num3 = 10;//定义静态局部变量,编译通过
int num4 = 0;//声明内部类普通成员变量
}
final class Inner3{}
abstract class Inner4{}
private class Inner5{}
protected class Inner6 {}
private class Inner7{}
}
2)可以调用外部类的结构
- 可以在内部定义属性、方法、构造器等结构。
- 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式
- 在别的类中,可以使用 外部类名.内部类名 对象名 = new 外部类名(). new 内部类名(); 的方式对内部类实例化,并引用内部类成员。
- 内部类在别的类声明后会先调用外部内的构造方法,然后直接实例化并不再调用内部类的构造器。
- 内部类可以直接调用外部类的方法和属性,若出现同名方法和属性需要补全 外部类.this.属性/方法 引用。
public class TestInner {
public static void main(String[] args) {
//外部类名.内部类名 对象名 = new 外部类对象. new 内部类对象
Outer.Inner oi = new Outer().new Inner();
oi.innerMethod("狗蛋");
}
}
//外部类
class Outer{
int age;
String name = "外部类的String属性";
public Outer() {
System.out.println("外部类的无参构造");
}
public Outer(int age, String name) {
System.out.println("外部类的全参构造");
}
public void outerMethod(){
System.out.println("外部类的方法");
}
//成员内部类
class Inner{
int age;
String name = "内部类的String属性";
public void Inner(){
System.out.println("内部类的无参构造");
}
public void Inner(int sum,String name){
System.out.println("内部类的有参构造");
}
public void innerMethod(String name){
System.out.println("内部类的方法");
System.out.println(name);//形参
System.out.println(this.name);//内部成员变量
System.out.println(Outer.this.name);//外部成员变量
outerMethod();
}
}
}
sout:
外部类的无参构造
内部类的方法
狗蛋
内部类的String属性
外部类的String属性
外部类的方法
3)内部类私有化的情况
- 内部类私有化后,不能被其他类直接调用,若想调用需要提供私有内部类对象的创建和方法的调用
- 私有内部类被包含在外部类之中,被看做是外部类的一个特殊成员,所以在外部类中可以直接创建私有内部类的对象并调用它的方法。
public class TestInner {
public static void main(String[] args) {
// 编译不通过, 内部类私有化后,不能被其他类直接调用
// Outer2.Inner2 oi = new Outer2().new Inner2();
// oi.eat();
//使用匿名对象调用,调用私有内部类对象的创建和方法
new Outer().getInnerMethod();
new Outer().new Inner1
}
}
class Outer{
//创建实例化内部类的成员方法
public void getInnerMethod(){
Inner in = new Inner();
in.innerMethod();//调用
}
//创建私有内部类
private class Inner{
public void innerMethod(){
System.out.println("内部类Inner的方法");
}
//创建公共内部类
public class Inner1{
public void innerMethod(){
System.out.println("内部类Inner1的方法");
}
}
}
编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)
格式:
成员内部类:外部类 $ 内部类名.class
局部内部类:外部类 $ 数字 内部类名.class
12.3 局部内部类
局部内部类(方法内部类)是指在成员方法中定义的类。
- 局部内部类只能在所在的成员方法中被实例化。
- 当其他类想使用局部内部类中的属性或方法,需要在局部内部类所在的成员方法中创建局部内部类的对象并且进行功能调用后,其他类通过调用该内部类的方法,才能间接调用。
public class TestInner {
public static void main(String[] args) {
new Outer().outerMethod();
}
}
class Outer{
public void outerMethod() {
//创建局部内部类Inner
class Inner{
//创建局部内部类的普通属性与方法
String name;
int age;
public void innerMethod() {
System.out.println("我是Inner的innerMethod()方法");
}
}
//在outerMethod()里创建内部类对象
Inner in = new Inner();
in.innerMethod();
System.out.println(in.name);
System.out.println(in.age);
}
}
12.4 匿名内部类
匿名内部类属于局部内部类,而且是没有名字的局部内部类,通常和匿名对象一起使用。
因为接口不可以创建对象。所以,当方法的参数被定义为一个接口类型,那么就需要定义一个类来实现接口,并根据该类进行对象实例化。除此之外,还可以使用匿名内部类来实现接口。
常规方式实现接口
- 接口的使用:创建接口实现类 +重写接口中的抽象方法 + 创建实现类对象 + 调用方法
匿名对象只使用一次,而且一次只能干一件事!
1)使用匿名内部类的方式实现接口
- 创建了一个接口的实现类 + 重写接口中的所有抽象方法
2)使用匿名内部类的方式实现抽象类,
- 相当于创建了抽象类的普通子类。匿名内部类实现的方法都是抽象方法,而且是所有抽象方法。
3)普通类的匿名对象,不会强制要求产生匿名内部类的重写方法
- 如果使用对象,只需要干一件事–可以直接创建匿名对象,简单又方便。
//创建接口
interface Inner1{
//定义接口中的抽象方法
void save();
void get();
}
//创建抽象类
abstract class Inner2{
public void play(){
System.out.println("我是Inner2抽象类中的普通方法play()");
}
abstract public void drink();
}
//创建普通类Inner3
class Inner3{
public void power(){
System.out.println("我们会越来越强的,光头强");
}
public void study(){
System.out.println("什么都阻挡不了我学习的脚步");
}
}
public class TestInner {
public static void main(String[] args) {
//new Inner1();//接口不能创建对象
//接口的匿名实现类
new Inner1() {
@Override
public void save() {//重写接口中的抽象方法1
System.out.println("我是Inner1接口的save()");
}
@Override
public void get() {//3.2重写接口中的抽象方法2
System.out.println("我是Inner1接口的get()");
}
}.get();//触发指定的重写后的方法,只能调用一个,并且只能调用一次
//抽象方法的匿名对象,相当于创建了抽象类的普通子类
/*匿名内部类实现的方法都是抽象方法,注意是所有抽象方法*/
new Inner2() {
@Override
public void drink() {
System.out.println("我是Inner2抽象类的drink()");
}
}.drink();
//普通类的匿名对象
new Inner3().power();
//如果想使用同一个对象来干很多件事情,必须要给对象起名字
Inner3 i3 = new Inner3();
i3.power();
i3.study();
i3.study();
i3.study();
}
}
/*sout
我是Inner1接口的get()
我是Inner2抽象类的drink()
我们会越来越强的,光头强
我们会越来越强的,光头强
什么都阻挡不了我学习的脚步
什么都阻挡不了我学习的脚步
什么都阻挡不了我学习的脚步*/