Java 抽象类和接口
内部类
定义及简单示例
在 Java 中,允许在一个类
、方法
、语句块
的内部定义另一个类,称为内部类(Inner Class)
或嵌套类(Nested Class)
。
内部类和外层封装它的类之间存在逻辑上的所属关系,一般只用在定义它的类或语句块之内,实现一些没有通用意义的功能逻辑,在外部引用它时必须给出完整的名称;
使用内部类的主要原因有:
- 内部类可以访问外部类中的数据,包括私有的数据;
- 内部类可以对同一个包中的其它类隐藏起来;
- 当想要定义一个回调函数时,使用
匿名内部类
比较便捷; - 内部类的使用可以减少类的命名冲突(命名真的很头疼);
内部类只是一颗语法糖,只是编译器的一种行为,内部类在编译后一样会生成常规的 .class 类文件,内部类与 JVM 无关。
使用内部类的例子:
对于实例成员类,必须先有外部类的对象才能生成内部类的对象,因为内部类需要访问外部类中的成员变量,成员变量必须实例化才有意义。
内部类的分类
内部类分为:实例成员类、静态成员类、具名局部类、匿名局部类;
实例成员类
、静态成员类
就像一个类中的普通成员(属性、方法),因此可以使用 public/protected/private/[default]、static、final、abstract 等修饰符;它们之间仅仅就是普通成员和静态成员的区别;具名局部类
、匿名局部类
就像一个普通的局部变量(方法、语句块中),因此它们和普通的局部变量一样,不能被 static、public/protected/private 修饰符修饰;但匿名局部类必须继承一个类或实现一个接口。
无论哪种内部类,实质都是一个独立 .class 字节码文件,本质就是一个拥有特殊类名($
美元符)的普通类。
实例成员类、静态成员类
在类内部(不在方法、语句块)直接定义的类就是成员类,和类的普通成员一样,有着静态成员与实例成员之分:
- 实例成员类:可以在内部类中直接引用外部类的所有成员(静态、非静态),因此必须先有外部类对象才能构造内部类对象;
- 静态成员类:可以在内部类中直接引用外部类的静态成员,静态内部类其实就是一个普通的全局类,只不过名字特殊点而已。
成员式内部类和普通成员一样,支持 public、protected、private、[default] 访问修饰符;支持 static、final、abstract 属性修饰符。
若有 static 修饰符,就为类级(即静态成员类
),否则为对象级(即实例成员类
);类级可以通过外部类直接访问,对象级需要先生成外部类对象后才能访问;
对于实例成员类,类内不能声明任何 static 成员;同一个外部类的不同的内部类之间可以互相调用,就如同类中的不同方法之间可以互相调用一样。
并且,外部类可以访问内部类的所有成员,包括被 private 修饰的成员,在上面的例子中已经进行演示。
创建实例成员类的对象
因为需要访问外部类的实例变量、实例方法,因此必须在外部类对象的基础上构造内部类对象。
语法:outObj.new Inner(param ...)
(具名对象)、new Outer(param ...).new Inner(param ...)
(匿名对象)。
创建静态成员类的对象
静态成员类就是特殊点的全局类,当然作为成员类还是有点特权的(可以访问外部类的所有静态成员,包括私有的)。
语法:new Inner(param ...)
(类内引用)、new Outer.Inner(param ...)
(类外引用,类的权限不能为 private)。
在内部类中访问外部类
具体的调用细节我已经在开头的例子中详细说明了,因为编译器搜索一个变量名时总是从近到远的搜索,因此内部类与外部类的成员存在同名时就需要显式的调用,否则默认使用内部类中的同名成员。
允许定义成员接口
因为接口是从抽象类(抽象类属于类)演变过来的,因此除非特别规定,接口和类享受着同样的待遇(接口的详细介绍在后面);
因此,Java 完全允许我们在一个外部类中定义内部接口(静态成员,不管写不写 static),不存在什么实例内部接口,没有意义;
也可以在外部接口中定义内部接口(公开静态成员,不管写不写 static),这个应该是应用最广泛的,Java 类库中有很多的例子。
具名局部类、匿名局部类
局部类就是定义在方法、语句块中的类,我先把局部类的相关特点说明一下:
- 局部类只在定义了它们的方法、语句块中可见;
- 外部作用域可以访问局部类的所有成员,包括私有的;
- 局部类不可以是 static 的,也不存在 static 成员;
- 局部类不可以被 public、protected、private 修饰;
- 具名局部类可以被 abstract 修饰,但不存在所谓局部接口;
- 匿名局部类必须继承一个类或实现一个接口,但只能二选一;
- 局部类会捕获外部作用域中的变量(值传递),在类内部使用
final
修饰。
前几点没什么好讲的,主要是最后两个。我们先来分析最后一条,解释一下”类内部使用final
修饰”。
先定义一个具名局部类 Student,暂时不涉及外部变量捕获:
和使用普通的 Student 类一样,都是通过函数传参来进行数据传递,没问题。
但是这样的话就发挥不了局部类的最大作用了啊,我们来让它自动捕获外部的变量:
是不是发现很好用,这就是局部类的便利之处,可以自动捕获外部作用域的变量,但是如果我要修改捕获的变量呢?
这里有几点要说明:如果被捕获的变量(意思就是在局部类中被使用的,未使用的不需要)是函数、代码块中的,那么这些变量必须被 final 修饰;如果被捕获的变量是在类中定义(静态、非静态)的,则无此要求。并且,在 JDK1.8 之后,函数、代码块中的被捕获变量不需要显式使用 final 修饰,会自动变为 final 变量。
报错了,意思是说”从内部类引用的局部变量必须是 final 的”。这就是局部类的猥琐之处;因为局部类捕获外部变量时采取的是值传递(C++ 的 lambda 表达式支持引用传递),因此无法修改变量值(对于引用类型就是无法改变指针的指向)。
Java 为什么要这么做呢,因为 Java 中只存在值传递。这就是抛弃指针运算的恶果(来自 C/C++ 程序猿的嘲笑)。换个角度理解就是前面一开始说的”在类内部使用final
修饰”。
编译器在编译之前做了手脚,上面的程序大概会变成这样(当然我不敢保证是这样,猜测而已):
这就很容易理解了,final 成员自然是不可以被改变了,因此在编译时会报错(虽然那个报错信息很诡异)。
但是被 final 修饰的引用类型变量,只是意味着不可以改变指针的指向,并没有说不让我们更改指向的内容。
我说这话是什么意思呢?别着急,我们看例子就知道了:
好了,具名局部类就说到这里,我们来简单的看一下匿名局部类,上面讨论的东西一样适合匿名局部类,后面不再重复演示。
匿名局部类,即没有名字的局部类,但实际上是有名字的,这个后面会进行讨论。匿名局部类不可单独存在,它必须是一个类的子类或者是实现了一个接口的类。
匿名局部类的特点
1、匿名局部类必须继承一个类或者实现一个接口,但两者不可兼得,同时也只能继承一个类或者实现一个接口;
2、匿名局部类没有构造函数,因为没有类名因此无法实现构造函数;可以使用初始化代码块替代构造函数;
3、匿名局部类是具名局部类的一种特殊形式,因此具名局部类的所有限制同样对匿名局部类有效;
4、匿名局部类不能是 abstract 的,它必须要实现继承的类或者实现的接口的所有抽象方法。
匿名内部类实际还是一个独立的类,它有着自己的唯一类名,如果将一个使用 new 创建的匿名类对象赋值给其基类/实现的接口的引用变量,那么此过程中将发生向上转型,因为匿名类就是 new 后面的类/接口的一个子类,它们之间的关系就如同普通的基类、派生类。
我们先来看一下第一种,一个类的子类:
再来看一下第二种,实现一个接口:
最后我们来说一下,如何利用反射创建匿名局部类的对象,反射的具体知识请参考 - Java 反射。
抽象类
在自上而下的继承层次结构中,位于上层的类更具有通用性,甚至可能更加抽象;
从某种角度看,祖先类更加通用,它只包含一些最基本的成员,人们只将它作为派生其他类的基类,而不会用来创建对象;
甚至,你可以只给出方法的定义而不实现,由子类根据具体需求来具体实现;
这种只给出方法定义而不具体实现的方法被称为抽象方法
,抽象方法是没有方法体的,在代码的表达上就是没有“{}”;
如果一个类包含一个或多个抽象方法就必须被声明为抽象类
;抽象类不能被实例化,抽象方法必须在子类中被实现;
使用abstract
修饰符来表示抽象方法
和抽象类
;抽象类除了包含抽象方法外,还可以包含具体的变量和具体的方法;
一个类即使没有任何抽象方法,也可以被声明为抽象类,防止被实例化;
抽象类不能直接使用,必须用子类去实现抽象类,然后使用其子类的实例;
可以创建一个变量,其类型是一个抽象类,并让它指向具体子类的一个实例,即多态的使用;
特别注意,abstract
不能和static
一起使用,即不能有抽象静态方法,也不能有抽象构造函数;
如果一个抽象基类拥有多个抽象方法,那么继承他的派生类也必须实现所有抽象方法,否则不能创建对象;
当然如果没有实现所有的抽象方法,也可以将派生类声明为抽象类,让它的派生类去实现剩下的抽象方法;
典型的错误:抽象类一定包含抽象方法;但是反过来说“包含抽象方法的类一定是抽象类”就是正确的;
事实上,抽象类可以是一个完全正常实现的类;
例子:
接口
在抽象类中,可以包含一个或多个抽象方法;但在接口(interface)
中,所有的方法必须都是抽象的,不能有方法体,它比抽象类更加“抽象”;
接口使用interface
关键字来声明,接口可以看做是一种特殊的抽象类,它可以指定一个类必须做什么,而不是规定它如何去做;
现实中也有很多接口的实例,比如说串口电脑硬盘,Serial ATA 委员会指定了 Serial ATA 2.0 规范,这种规范就是接口;
Serial ATA 委员会不负责生产硬盘,只是指定通用的规范;
希捷、日立、三星等生产厂家会按照规范生产符合接口的硬盘,这些硬盘就可以实现通用化;
如果正在用一块 160G 日立的串口硬盘,现在要升级了,可以购买一块 320G 的希捷串口硬盘,安装上去就可以继续使用了;
接口是若干常量
和抽象方法
的集合
接口中声明的成员变量
默认都是public static final
的,必须显示的初始化;因而在常量声明时可以省略这些修饰符;
接口中声明的成员函数
默认都是public abstract
的,不能提供任何方法体,需要让实现接口的子类去具体定义;
为什么使用接口
接口是可插入性
的保证;在一个继承链中的任何一个类都可以实现一个接口,这个接口会影响到此类的所有子类,但不会影响到此类的任何父类;此类将不得不实现这个接口所规定的方法,而子类可以从此类自动继承这些方法,这时候,这些子类具有了可插入性;
我们关心的不是哪一个具体的类,而是这个类是否实现了我们需要的接口;
接口提供了关联以及方法调用上的可插入性,软件系统的规模越大,生命周期越长,接口使得软件系统的灵活性和可扩展性,可插入性方面得到保证;
接口在面向对象的 Java 程序设计中占有举足轻重的地位;事实上在设计阶段最重要的任务之一就是设计出各部分的接口,然后通过接口的组合,形成程序的基本框架结构;
接口的使用
接口的使用与类的使用有些不同;在需要使用类的地方,会直接使用 new 关键字来构建一个类的实例,但接口不可以这样使用,因为接口不能直接使用 new 关键字来构建实例;
接口必须通过类来实现(implements)
它的抽象方法,然后再实例化类;类实现接口的关键字为implements
;
如果一个类不能实现该接口的所有抽象方法,那么这个类必须被定义为抽象类
;
不允许创建接口的实例,但允许定义接口类型的引用变量,该变量指向了实现接口的类的实例;
接口和抽象类、普通类的共同点
目前看来和抽象类差不多;确实如此,接口本就是从抽象类中演化而来的,因而除特别规定,接口享有和类同样的“待遇”;
比如一个文件中可以定义多个类或接口,但最多只能有一个 public 的类或接口,如果有则源文件必须取和 public 的类或接口相同的名字;
但是接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念;类描述对象的属性和方法;接口则包含类要实现的方法;
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法;
接口无法被实例化,但是可以被实现;一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类;
接口的特性
但是接口有自己的一些特性,归纳如下:
1) 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为public abstract
,并且只能是public abstract
;
2) 接口中可以含有变量,但是接口中的变量会被隐式的指定为public static final
静态常量,并且只能是public static final
;
3) 接口中没有构造方法,不能被实例化;
4) 一个接口不实现另一个接口,但可以继承多个其他接口;接口
的多继承
特点弥补了类
的单继承
;
5) 一个类只能继承一个父类,但却可以实现多个接口;实现接口使用关键字implements
;
接口和抽象类的区别
1) 抽象类本质还是一个类,但是接口不是类;
2) 抽象类可以有构造函数,但是接口没有构造函数;
3) 抽象类可以有具体的方法,但是接口中只能有抽象方法;
4) 抽象类中的成员变量可以是各种修饰符的,但是接口中的成员变量只能是 public static final 的;
5) 抽象类中可以有 static 代码块和 static 方法,但是接口中不能有 static 代码块和 static 方法;
6) 一个类只能继承一个抽象类,而一个类却可以实现多个接口;
接口的声明格式public/[default] interface 接口名称 [extends 接口1[, 接口2, ...]] { 声明变量、抽象方法 }
类实现接口的格式[修饰符] class 类名 [extends 父类] implements 接口1[, 接口2, ...] { 实现抽象方法 }
重写接口中声明的方法时,需要注意以下规则:
1) 类在实现接口的方法时,不能抛出强制性异常
,只能在接口
中,或者继承接口的抽象类
中抛出该强制性异常;
2) 类在重写方法时要保持一致的方法名,并且应该保持相同或者相兼容的返回值类型;即应遵循方法重写
的规则;
标记接口
标识接口是没有任何方法和属性的接口,它仅仅表明它的类属于一个特定的类型,供其他代码来测试允许做一些事情;
标识接口作用:简单形象的说就是给某个对象打个标(盖个戳),使对象拥有某个或某些特权;
没有任何方法的接口被称为标记接口;标记接口主要用于以下两种目的:
1) 建立一个公共的父接口;
2) 向一个类添加数据类型;
这种情况是标记接口最初的目的,实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法),但是该类通过多态性变成一个接口类型;
如何选择抽象类和接口
接口和抽象类各有优缺点,在接口和抽象类的选择上,必须遵守这样一个原则:
1) 行为模型应该总是通过接口而不是抽象类定义,所以通常是优先选用接口,尽量少用抽象类;
2) 选择抽象类的时候通常是如下情况:需要定义子类的行为,又要为子类提供通用的功能;
例子: