1.接口的思想
前面学习了接口的代码体现,现在来学习接口的思想,接下里从生活中的例子进行说明。
举例:我们都知道电脑上留有很多个插口,而这些插口可以插入相应的设备,这些设备为什么能插在上面呢?主要原因是这些设备在生产的时候符合了这个插口的使用规则,否则将无法插入接口中,更无法使用。发现这个插口的出现让我们使用更多的设备。
总结:接口在开发中的它好处
- 接口的出现扩展了功能。
- 接口其实就是暴漏出来的规则。
- 接口的出现降低了耦合性,即设备与设备之间实现了解耦。
接口的出现方便后期使用和维护,一方是在使用接口(如电脑),一方在实现接口(插在插口上的设备)。例如:笔记本使用这个规则(接口),电脑外围设备实现这个规则(接口)
2.接口与抽象类的区别
通过实例进行分析和代码演示抽象类和接口的用法。
1. 举例
犬:
行为:
吼叫;
吃饭;
缉毒犬:
行为:
吼叫;
吃饭;
缉毒;
2. 思考
由于犬分为很多种类,他们吼叫和吃饭的方式不一样,在描述的时候不能具体化,也就是吼叫和吃饭的行为不能明确,描述行为时,行为的具体动作不能明确,可以将这个行为写为抽象行为,那么这个类也就是抽象类。
可是当犬有其他额外功能时,而这个功能并不在这个事物的体系中。这时可以让犬具备犬科自身特点的同时也有其他额外功能,可以将这个额外功能定义接口中。
interface 缉毒{
void 缉毒();
}
//定义犬科的这个提醒的共性功能
abstract class 犬科{
abstract void 吃饭();
abstract void 吼叫();
}
// 缉毒犬属于犬科一种,让其继承犬科,获取的犬科的特性,
//由于缉毒犬具有缉毒功能,那么它只要实现缉毒接口即可,这样即保证缉毒犬具备犬
科的特性,也拥有了缉毒的功能
class 缉毒犬 extends 犬科 implements 缉毒{
public void 缉毒() {
}
void 吃饭() {
}
void 吼叫() {
}
}
class 缉毒猪 implements 缉毒{
public void 缉毒() {
}
}
3. 通过上面的例子总结接口和抽象类的区别
相同点:
都位于继承的顶端,用于被其他实现或继承;
都不能实例化;
都包含抽象方法,其子类都必须覆写这些抽象方法;
区别:
抽象类为部分方法提供实现,避免子类重复实现这些方法,提供代码重用性;接口只能包含抽象方法;
一个类只能继承一个直接父类(可能是抽象类),却可以实现多个接口;(接口弥补了Java的单继承)
二者的选用:
优先选用接口,尽量少用抽象类;
需要定义子类的行为,又要为子类提供共性功能时才选用抽象类。
3.多态
多态由来
//描述狗,狗有吃饭,看家的行为
class Dog
{
public void eat()
{
System.out.println("啃骨头");
}
public void lookHome()
{
System.out.println("看家");
}
}
//描述猫,猫有吃饭,抓老鼠行为
class Cat
{
public void eat()
{
System.out.println("吃鱼");
}
public void catchMouse()
{
System.out.println("抓老鼠");
}
}
class DuoTaiDemo
{
public static void main(String[] args)
{
/*有多个狗和猫对象都要调用吃饭这个行为
这样会导致d.eat();代码重复性非常严重
Dog d = new Dog();
d.eat();
为了提高代码的复用性,可以将d.eat();代码进行封装
public static void method(Dog d)
{
d.eat();
}
然后创建对象,直接调用method方法即可
method(new Dog());
method(new Dog());
但当创建Cat对象时,同样需要调用eat方法,同样可以将eat方法进行封装
public static void method(Cat c)
{
c.eat();
}
*/
}
}
后期当有了猪对象时,那么同样要封装eat方法,当每多一个动物,都要单独定义功能,封装方法让动物的对象去做事,会发现代码的扩展性很差。如何提高代码的扩展性呢?发现既然是让动物去eat,无论是dog,还是cat,eat是他们的共性,那么将eat进行抽取,抽取到父类中。
abstract class Animal
{
//由于每一个小动物的eat方式都不一样,因此在父类中无法准确描述eat的具体
行为
//因此只能使用抽象方法描述,从而导致这个类也为抽象类
abstract public void eat();
}
当有了Animal抽象类之后,狗和猫只要继承这个类,实现他们特有的eat方法即可。
//描述狗,狗有吃饭,看家的行为
class Dog extends Animal
{
public void eat()
{
System.out.println("啃骨头");
}
public void lookHome()
{
System.out.println("看家");
}
}
//描述猫,猫有吃饭,抓老鼠行为
class Cat extends Animal
{
public void eat()
{
System.out.println("吃鱼");
}
public void catchMouse()
{
System.out.println("抓老鼠");
}
}
既然Dog属于Animal中一种,Cat也属于Animal中一种,那么不用具体面对具体的动物,而只要面对Animal即可。
Dog d = new Dog();
Animal a = new Dog();
Cat c = new Cat();
Animal aa = new Cat();
通过上述代码发现,Animal类型既可以接受Dog类型,也可以接受Cat类型,当再让动物去做事时,不用面对具体的动物,而只要面对Animal即可。因此上述method方法可以修改为:
public static void method(Animal a)
{
a.eat();
}
method(Animal a)可以接受Animal的子类型的所有小动物,而method方法不用在关心是具体的哪一个类型。即就是只建立Animal的引用就可以接收所有的Dog和Cat对象进来,让它们去eat。从而提高了程序的扩展性。
其实上述代码就已经形成了多态
父类的引用或者接口的引用指向了自己的子类对象。
Dog d = new Dog();//Dog对象的类型是Dog类型。
Animal a = new Dog();//Dog对象的类型右边是Dog类型,左边Animal类型。
多态的好处
提高了程序的扩展性。
多态的弊端
通过父类引用操作子类对象时,只能使用父类中已有的方法,不能操作子类特有的方法。
多态的前提
- 必须有关系:继承,实现。
- 通常都有重写操作
当父类的引用指向子类对象时,就发生了向上转型,即把子类类型对象转成了父类类型。向上转型的好处是隐藏了子类类型,提高了代码的扩展性。
但向上转型也有弊端,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。
//描述动物这种事物的共性eat
abstract class Animal{
abstract void eat();
}
//描述dog这类事物
class Dog extends Animal{
void eat(){
System.out.println("啃骨头");
}
void lookHome(){
System.out.println("看家");
}
}
//描述小猫
class Cat extends Animal{
void eat(){
System.out.println("吃鱼");
}
void catchMouse(){
System.out.println("抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //这里形成了多态
a.eat();
//a.lookHome();//使用Dog特有的方法,需要向下转型
Dog d = (Dog)a;
d.lookHome();
Animal a1 = new Cat();
a1.eat();
/*
由于a1具体指向的是Cat的实例,而不是Dog实例,这时将a1强制转成Dog
类型,将会发生ClassCastException异常,在转之前需要做健壮性判断
if(!Dog instanceof a1){ // 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;// 方法执行中止
}
Dog d1 = (Dog)a1;
d1.catchMouse();
*/
}
}
什么时候使用向上转型
当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型。
什么时候使用向下转型
当要使用子类特有功能时,就需要使用向下转型。
向下转型的好处:可以使用子类特有功能。
弊端是:需要面对具体的子类对象;在向下转型时容易发生ClassCastException类型转换异常。在转换之前必须做类型判断
举例
/*
描述毕老师和毕姥爷,
毕老师拥有讲课和看电影功能
毕姥爷拥有讲课和钓鱼功能
*/
class 毕姥爷{
void 讲课(){
System.out.println("政治");
}
void 钓鱼(){
System.out.println("钓鱼");
}
}
//毕老师继承了毕姥爷,就有拥有了毕姥爷的讲课和钓鱼的功能,
//但毕老师和毕姥爷的讲课内容不一样,因此毕老师要覆盖毕姥爷的讲课功能
class 毕老师 extends 毕姥爷{
void 讲课(){
System.out.println("Java");
}
void 看电影(){
System.out.println("看电影");
}
}
public class Test {
public static void main(String[] args) {
//多态形式
毕姥爷 a = new 毕老师(); //向上转型
a.讲课(); // 这里表象是毕姥爷,其实真正讲课的仍然是毕老师,因此调用的
也是毕老师的讲课功能
a.钓鱼(); // 这里表象是毕姥爷,但对象其实是毕老师,而毕老师继承了毕姥
爷,即毕老师也具体钓鱼功能
// 当要调用毕老师特有的看电影功能时,就必须进行类型转换
毕老师 b = (毕老师)a; //向下转型
b.看电影();
}
}
总结:转型过程中,至始至终只有毕老师对象做着类型转换,父类对象是无法转成子类对象的。
/*
描述笔记本,笔记本使用USB鼠标,USB键盘
定义USB接口,笔记本要使用USB设备,即笔记本在生产时需要预留可以插入USB
设备的USB接口,即就是笔记本具备使用USB设备的功能,但具体是什么USB设备,笔记本
并不关心,只要符合USB规格的设备都可以。鼠标和键盘要能在电脑上使用,那么鼠标和键
盘也必须遵守USB规范,不然鼠标和键盘的生产出来无法使用
*/
//定义鼠标、键盘,笔记本三者之间应该遵守的规则
interface USB{
void open();//开启功能
void close();//关闭功能
}
//鼠标实现USB规则
class Mouse implements USB{
public void open(){
System.out.println("鼠标开启");
}
public void close(){
System.out.println("鼠标关闭");
}
}
//键盘实现USB规则
class KeyBoard implements USB{
public void open(){
System.out.println("键盘开启");
}
public void close(){
System.out.println("键盘关闭");
}
}
// 定义笔记本
class NoteBook{
//笔记本开启运行功能
public void run(){
System.out.println("笔记本运行");
}
//笔记本使用usb设备,这时当笔记本实体调用这个功能时,必须给其传递一个符
合USB规则的USB设备
public void useUSB(USB usb){
//判断是否有USB设备
if(usb != null){
usb.open();
usb.close();
}
}
public void shutDown(){
System.out.println("笔记本关闭");
}
}
public class Test {
public static void main(String[] args) {
//创建笔记本实体对象
NoteBook nb = new NoteBook();
//创建鼠标实体对象
Mouse m = new Mouse();
//创建键盘实体对象
KeyBoard kb = new KeyBoard();
//笔记本开启
nb.run();
//笔记本使用鼠标
nb.useUSB(m);
//笔记本使用键盘
nb.useUSB(kb);
//笔记本关闭
nb.shutDown();
}
}
多态中成员的特点
多态出现后会导致子父类中的成员变量有微弱的变化。看如下代码
class Fu
{
int num = 4;
}
class Zi extends Fu
{
int num = 5;
}
class Demo
{
public static void main(String[] args)
{
Fu f = new Zi();
System.out.println(f.num);
Zi z = new Zi();
System.out.println(z.num);
}
}
多态成员变量
当子父类中出现同名的成员变量时,多态调用该变量时:
编译时期:参考的是引用型变量所属的类中是否有被调用的成员变量。没有,编译失败。
运行时期:也是调用引用型变量所属的类中的成员变量。
简单记:编译和运行都参考等号的左边。编译运行看左边。
class Fu
{
int num = 4;
void show()
{
System.out.println("Fu show num");
}
}
class Zi extends Fu
{
int num = 5;
void show()
{
System.out.println("Zi show num");
}
}
class Demo
{
public static void main(String[] args)
{
Fu f = new Zi();
f.show();
}
}
多态成员函数
编译时期:参考引用变量所属的类,如果没有类中没有调用的函数,编译失败。
运行时期:参考引用变量所指的对象所属的类,并运行对象所属类中的成员函数。
简而言之:编译看左边,运行看右边。
class Fu
{
int num = 4;
static void method()
{
System.out.println("fu static method run");
}
}
class Zi extends Fu
{
int num = 5;
static void method()
{
System.out.println("zi static method run");
}
}
class Demo
{
public static void main(String[] args)
{
Fu f = new Zi();
f.method();
}
}
多态静态函数
多态调用时:编译和运行都参考引用类型变量所属的类中的静态函数。
简而言之:编译和运行看等号的左边。其实真正调用静态方法是不需要对象的,静态方法通过类直接调用。
结论:
- 对于成员变量和静态函数,编译和运行都看左边。
- 对于成员函数,编译看左边,运行看右边。
4.内部类
什么是内部类
将类写在其他类的内部,可以写在其他的成员位置和其他类的局部位置,这时写在其他类内部的类就称为内部类。
什么时候使用内部类
在描述事物,若一个事物内部还包含其他可能包含的事物,比如在描述汽车时,汽车中还包含这发动机这个事物,这时发动机就可以使用内部类来描述。即就是内部事物必须寄宿在外部事物内部。
内部类代码体现
class Outer{
//外部类的成员变量
int num = 5;
//写在Outer成员位置上的内部类
class Inner{
//内部类的成员函数
void show(){
//在内部类中访问外部类的成员变量
System.out.println("Outer num = "+num);
}
}
}
内部类访问规则
内部类可以直接访问外部类中的成员,但外部类不能直接访问内部类,若要访问,必须创建内部类对象才能访问。
public class Test {
public static void main(String[] args) {
Outer out = new Outer();
out.method();
}
}
class Outer{
//外部类的成员变量
int num = 5;
//写在Outer成员位置上的内部类
class Inner{
//内部类的成员函数
void show(){
//在内部类中访问外部类的成员变量
System.out.println("Outer num = "+num);
}
}
public void method(){
//创建内部类对象,访问内部类的成员函数或者变量
Inner in = new Inner();
in.show();
}
}
非静态非私有内部类
当内部类在外部类成员位置上的时候,内部类就是外部类成员的一份子。这时这个内部类就可以使用成员修饰符修饰,比如public、static、private
如果内部类的权限是非私有的,就可以在外部类以外的其他类中访问。即可以通过创建外部类对完成访问内部类。
比如以上程序:就可以使用如下格式访问内部类的show方法。
public class Test {
public static void main(String[] args) {
//通过创建外部类对象,接着创建内部类对象
Outer.Inner in = new Outer().new Inner();
in.show();
}
}
class Outer{
//外部类的成员变量
int num = 5;
//写在Outer成员位置上的内部类
class Inner{
//内部类的成员函数
void show(){
//在内部类中访问外部类的成员变量
System.out.println("Outer num = "+num);
}
}
public void method(){
//创建内部类对象,访问内部类的成员函数或者变量
Inner in = new Inner();
in.show();
}
}
静态的非私有内部类
当内部类在外部类成员位置上被static修饰时,由于静态可以直接使用类名调用,则创建内部类对象的方式:
public class Test {
public static void main(String[] args) {
//因为内部类是静态,所以不需要创建Outer的对象。直接创建内部类对象
就哦了。
Outer.Inner in = new Outer.Inner();
in.show();
}
}
class Outer{
//外部类的成员变量
static int num = 5;
//写在Outer成员位置上的静态内部类
static class Inner{
//内部类的成员函数
void show(){
//在内部类中访问外部类的成员变量
System.out.println("Outer num = "+num);
}
}
}
访问静态内部类的静态成员
由于内部类是静态的,可以直接使用外部类名调用内部类,而内部类的成员也是静态的,这时同样可以通过类名调用内部类的静态成员。
public class Test {
public static void main(String[] args) {
//既然静态内部类已随外部类加载,而且静态成员随着类的加载而加载,就不需要
对象,直接用类名调用即可
Outer.Inner.show2();
}
}
class Outer{
//外部类的成员变量
static int num = 5;
//写在Outer成员位置上的静态内部类
static class Inner{
//内部类的静态方法
static void show2(){
System.out.println("static Inner method num = "+num);
}
}
}
使用static修饰内部类,该内部类属于其外部类,而不属于外部类的对象;
静态内部类可包括静态成员也可包括非静态成员。根据静态成员不能访问非静态成员的规定,所以静态内部类不能访问外部类实例成员,只能访问外部类的静态成员。
非静态内部类细节
注意非静态内部类中不能定义静态成员变量和静态成员函数。但可以定义静态成员常量。原因常量在生成字节码文件时直接就替换成对应的数字。
当内部类在外部类成员位置上被私有修饰,在外部类意外的其他地方是无法访问的。只能在外部类中访问
面试常见的问题
以下代码运行中,请注意如何访问对应的num变量
public class Test {
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.show();
}
}
class Outer {
int num = 5;// 外部类的成员变量
class Inner {
int num = 6;// 内部类的成员变量
void show() {
int num = 7; // 内部类局部变量
System.out.println("内部类局部num=" + num);
System.out.println("内部类成员num=" + this.num);
System.out.println("外部类成员num=" + Outer.this.num);
}
}
}
为什么内部类可以直接访问外部类的成员,那时因为内部类持有外部类的引用(外部类.this)。对于静态内部类不持有 外部类.this 而是直接使用 外部类名。