类的扩展
从函数的容器->接口的本质
我们将对象看作属于某种数据类型,并按该类型进行操作,在一些情况下,并不能反映对象以及对象操作的本质。很多时候我们并不关心对象的类型,而是能力,类型并不重要。
- 比如拍照,很多时候,只需要拍出符合需求的照片即可,至于是用手机拍,还是用单反拍,并不重要,即关心的是对象能否有拍照的能力,而不并关心对象是什么类型,手机或单反都可以。
- 比如计算数字,只要能计算出正确的结果即可,至于是由口算还是计算器算,并不重要,即关心的是对象能否有计算的能力,而并不关心对象是算盘还是计算器。
在某些情况下,类型并不重要,重要的是能力。
接口的概念
接口声明了一组能力,但仅仅体现在声明上,并没有实现这个能力,就像是一个约定。接口涉及的交互双方对象,一方需要实现接口,一方使用这个接口,但双方并没有直接互相依赖,它们只是通过接口间接交互。
拿USB接口来说,USB协议约定USB设备需要实现的能力,每个USB设备都需要实现这个能力,计算机使用USB协议与USB设备交互,计算机与USB设备互不依赖,但可以与USB设备交互。
接口的定义
很对对象都可以比较,对于求最大值,最小值,排序而言。他们并不关心对象的类型是什么,只要对象可以比较就可以了,或者说,他们关心对象有没有可比较的能力。
public interface Comparable {
/**
* @param anotherObject 表示另一个参与比较的对象
* 第一个参与比较的对象是自己
* 返回结果是int类型,
* 1表示大于参数对象,
* 0表示等于参数对象,
* -1表示小于参数对象
*/
int compareTo(Object anotherObject);
}
定义接口的代码解释如下:
- 使用
interface
关键字来声明接口,访问修饰符一般是public - interface后面就是接口的名字,示例中使用的是
Comparable
- 在接口定义中,声明了一个方法
compareTo
,但并没有方法实现(没有方法体),Java8之前,接口内不能实现方法。接口方法不需要加修饰符,加与不加都是public abstract
修饰。
实现接口
类可以实现接口,表示类的对象具有接口所表示的能力。
package com.shaw.i;
public class Student implements Comparable {
private int age;
private int javaScore;
private String name;
public Student(int age, int javaScore, String name) {
this.age = age;
this.javaScore = javaScore;
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", javaScore=" + javaScore +
", name='" + name + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getJavaScore() {
return javaScore;
}
public void setJavaScore(int javaScore) {
this.javaScore = javaScore;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int compareTo(Object anotherObject) {
if (!(anotherObject instanceof Student)) {
throw new IllegalArgumentException();
}
Student anotherStudent = (Student) anotherObject;
return this.getJavaScore() - anotherStudent.getAge();
}
}
代码解释如下:
- Java使用
implements
这个关键字表示实现接口,后面是接口名 - 实现接口必须要实现接口中声明的方法,Student类实现了
compareTo
方法。 - 一个类可以实现多个接口,表示类具有多种能力,各个接口使用
,
分割。
使用接口
接口不能new
,也就是说不能创建一个接口的对象,对象只能通过类来创建。但可以声明接口类型的引用,指向接口实现类的对象。比如:
Comparable cpr = new Studnet(18, 100, "shaw");
cpr是Comparable类型的引用,但指向了Student类型的对象,之所以能赋值时因为Student实现了Comparable接口。如果一个类实现了多个接口,那么这种类型的对象可以赋值给任意实现接口类型的变量。cpr可以调用Comparable接口声明的方法,也只能调用Comparable声明的方法,实际执行时,执行的是具体的代码。
为什么一定要接口类型的变量呢?在一些程序中,代码并不知道具体的类型,这才是接口发挥的威力。
package com.shaw.i;
public class CompUtil {
public static Object max(Comparable[] elements) {
if (elements == null || elements.length == 0) {
return null;
}
Comparable max = elements[0];
for (int i = 1; i < elements.length; i++) {
if (max.compareTo(elements[i]) < 0) {
max = elements[i];
}
}
return max;
}
public static void sort(Comparable[] elements) {
if (elements == null || elements.length == 0) {
return;
}
for (int i = 0; i < elements.length; i++) {
int minIndex = i;
for (int j = i + 1; j < elements.length; j++) {
if(elements[j].compareTo(elements[i]) < 0){
minIndex = j;
}
if(minIndex != i){
Comparable temp = elements[i];
elements[i] = elements[minIndex];
elements[minIndex] = temp;
}
}
}
}
}
类ComUtil提供了两个方法,max获取数组中最大的值,sort对数组进行排序,参数都是Comparable类型的数组,sort方法使用的是简单选择算法。
可以看出,这个类是针对Comparable接口进行编程,它并不知道具体的类型是什么,也并不关心,但却可以对任意实现了Comparable接口的类型进行操作。
我们来看如何针对Comparable实现类型Student进行操作,代码如下:
public static void main(String[] args) {
Student[] students = new Student[]{new Student(19, 55, "zs"), new Student(19, 99, "ls"), new Student(19,77, "ww")};
System.out.println(CompUtil.max(students));
CompUtil.sort(students);
System.out.println(Arrays.toString(students));
// Student{age=19, javaScore=99, name='ls'}
// [Student{age=19, javaScore=55, name='zs'}, Student{age=19, javaScore=77, name='ww'}, Student{age=19, javaScore=99, name='ls'}]
}
这里演示的是针对Comparable实现类Student数组的操作,实际上可针对所有Comparable接口的类型数组进行操作。这就是接口的威力,可以说,针对接口而非具体类型进行编程,是一种重要的思维方式。接口很多时候反应了对象以及对对象操作的本质。它的优点很多,首先是代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象具有相同的能力,如CompUtil。
接口更加重要的是降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非具体实现类型,程序可以根据情况替换接口,而不影响使用者。解决问题的关键步骤是分而治之,将复杂的大问题分解为小问题,但小问题之间不可能一点关系没有,分解的核心就是要降低耦合,提高灵活性,接口为恰当分解提供了有力的工具!!!。
接口的细节
接口中的变量
接口中可以定义变量,语法如下:
public interface Interface1{
public static final int a = 0;
}
这里定义一个变量a,修饰符是publci static final
,这个修饰符是可选的,即使不写,也是public static final。这个变量可以通过接口名.变量名
的方式使用,如interface1.a
。
接口的继承
接口也可以继承,一个接口也已继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以表示多个父接口,代码如下:
public interface IBase1{
void method1();
}
public interface IBase2{
void method1();
}
public interface IChild extends IBase1,IBase2{
}
IChild有IBase1和IBase2两个父类,接口的继承同样通过extends关键字表示,多个父接口之间使用,
隔开。
类的继承与接口
类的继承与接口实现可以共存,也就是说,类在继承基类的情况下,可以同时实现一个或多个接口,语法如下:
public class Child extends Base implements IBase{
}
关键字extends
要放在implemtents
之前。
instanceof
与类一样,接口也可以使用instanceof
关键字,用来判断一个对象是否实现了某个接口,例如:
Student s = new Studnet(18, 100, "shaw");
if(s instanceof Comparable){
print("Comparable");
}
使用接口代替继承
继承至少有两个好处:
- 代码复用
- 利用多态和动态绑定统一处理多种不同子类的对象
d而将组合和接口接口一起来替代继承,就既可以统一处理,又可以复用代码了。
public interface IAdd{
void add(int number);
void addAll(int[] numbers);
}
public class Base implements IAdd{
}
public class Child implements IAdd{
}
Child复用了Base的代码,又都实现了IAdd接口,这样,即复用代码,又可以统一处理,还不用担心破坏封装。
Java 8 和 Java 9对接口的增强
在Java 8 之前,接口中都是抽象方法,都没有实现体,Java 8允许在接口中定义两个新方法:静态方法和默认方法,它们都有实现体,比如:
public interface IDemo{
void hello();
public static void test(){
print("hello");
}
default void hi(){
print("hi");
}
}
test是一个静态方法,可以通过接口名.方法名
调用,比如IDemo.test()。在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Java API中,Collections接口中有一个对应的单独的类Collections,在Java 8中,就可以直接写在接口中了,比如Comparator接口就定义了多个静态方法。
hi()是一个默认方法,用关键字default修饰。默认方法与抽象方法都是接口的方法,不同在于,默认方法有默认的实现,实现类可以改变它的默认实现,也可以不改变。引入默认方法主要是函数式数据处理的需求,是为了方便个接口增加功能。
在没有默认方法之前,Java是很难给接口增加功能的,比如List接口,因为有太多非Java JDK控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java上运行,必须改写代码,实现新的方法,这显示无法接受的。函数式数据需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新的方法,而接口现有的实现类也不需要必须实现。看一些例子,List接口增加了sort方法,其定义为:
default void sort(Comparator<? suepr E> c){
Object[] a = this.toArray();
Arrays.sort(a, (Comparator)c);
ListIterator<E> i = this.listIterator();
for(Object e : a){
i.next();
i.set((E)e);
}
}
Collection增加了stream方法,定义为:
default Stream<E> stream(){
return StreamSupport.stream(spliterator(), false);
}
在Java 8中,默认方法和静态方法都必须是public的,Java 9去除了这个限制,它们都可以是private的,引入private方法主要是为了方便多个静态或默认方法复用代码,比如:
public interface IDemoPrivate {
private void common(){
print("common");
}
default void actionA(){
common();
}
default void actionB(){
common();
}
}
actionA和actionB两个默认方法共享了相同的common()方法的代码。
小结
讨论了数据类型思维的局限,提到了很多时候关心的是能力,而非类型,所以引入了接口。针对接口编程是一种重要的思维方式,这里方式不仅可以复用代码,还可以降低耦合,提高灵活性,是一种分解复杂问题的一种重要工具。
抽象类
介于接口和类之间的概念。
顾名思义,抽象类就是抽象的类,抽象类是相对于具体而言的,一般而言,具体类都有对象,而抽象类没有,它表达的是抽象概念。一般具体的类是比较上层的父类。比如,狗是具体的对象,而动物是抽象的概念;苹果是具体的对象;而水果是抽象的概念。
抽象类和抽象方法
只有子类才知道如何实现的方法,一般被定义为抽象方法。抽象方法是相对具体方法而言的,具体方法有代码,而抽象方法只有声明,没有实现。之前我们提到接口中的方法(非Java 8 引入的静态方法和默认方法)就都是抽象方法。
抽象类和抽象方法都是使用abstract
修饰的,语法如下:
public abstract class Shape(){
public abstract void draw();
}
定义了抽象方法的类必须声明为抽象类,不过,抽象类可以没有抽象方法。抽象类和具体类一样,可以定义具体方法,实例变量等,它和具体类的核心区别是,抽象类不能创建对象(比如,不能使用new Shape()),而具体类可以。
抽象类不能创建对象,要创建对象,可以使用具体的子类。一个类在继承抽象类之后,必须重写抽象类中的抽象方法,除非子类也声明为抽象类。
public class Circle extends Shape(){
@Override
public void draw(){
}
}
与接口相似,虽然不能new,但可以是抽象类引用,指向具体的子类。
Shapre shape = new Circle();
shape.draw();
为什么需要抽象类
引入抽象类和抽象方法,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道必须要重写这些方法,而不可能忽略。使用抽象类,类的使用者在创建对象的时候,就知道必须要使用具体的子类,而不可能误用不完整的父类。
抽象类和接口
抽象类和接口有类似之处:都不能创建对象,接口中的方法其实都是抽象方法。如果抽象类中只定义了抽象方法,那么和接口就更像了。抽象类和接口根本上是不同的,接口中不能定义实例变量,而抽象类可以,一个类可以实现多个接口,但只能继承一个类。抽象类和接口是配合而非替代关系,他们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如:
- Collection接口和对应的AbstractCollection抽象类。
- List接口和对应的AbstractList抽象类。
- Map接口和对应的AbstractMap抽象类。
- StringBuilder和StringBuffer继承了AbstractStringBuilder抽象类。
对于需要实现接口的具体类而言,有两个选择:一个是实现接口,自己实现全部方法;一个是继承抽象类;然后根据需要重写方法。继承的好处的复用代码,只重写需要的部分即可。
小结
内部类的本质
一般而言一个类都有一个对应的文件,但一个类还可以放在另一个类的内部,称为内部类,包含它的类称为外部类。
一般而言,内部类与包含它的外部类有密切的关系,而与其他类关系不大,定义在类内部,可以实现对外部完全隐藏,可以有更好的封装性,代码实现上往往也更简洁。
不过,内部类只是Java编译器的概念,对于Java虚拟机而言,它是不知道内部类这回事的,每个内部类最后都会被编译成一个独立的类,生成一个独立的字节码文件。
也就是说,每个内部类其实都可以被替换成一个独立的类。内部类可以方便的访问外部类的私有变量,可以声明为private从而实现对外隐藏,相关代码写在一起,写法更为简洁,这些都是内部类的好处
内部类根据定义的位置和方式不同,可以分为以下4种:
- 静态内部类
- 成员内部类
- 方法内部类
- 匿名内部类
其中,方法内部类是在一个方法内定义和使用的;匿名内部类使用范围更小;他们都不能在外部使用;成员内部类和静态内部类可以被外部使用,但可以声明为private。
静态内部类
静态内部类与静态变量和静态方法定义一样,使用static
关键字,只是它定义的是类。
public class Outer{
private static int shared = 100;
public static class Inner{
public void innerMethod(){
print("inner"+shared);
}
}
public void test(){
Inner inner = new Inner();
inner.innerMethod();
}
}
外部类为Outer,静态内部类为Inner,带有static修饰符。语法上,静态内部类除了位置放在其他类内部外,它与一个独立的类差别不大,可以有静态方法、静态属性、成员属性、成员方法、构造方法等。
静态内部类与外部类联系也不大。它可以访问外部类的静态属性和方法,如innerMethod()
方法,但不可以访问实例属性和方法。在类内部,可以直接使用静态内部类,如test()
方法。
public修饰的静态内部类可以被外部使用,只需要通过外部类.静态内部类
的方式即可使用。
Outer.Inner inner = new Outer.Inner();
实例代码实际上会生成两个类:一个是Outer,另一个是Outer$Inner,代码如下:
public class Outer{
private static int shared = 100;
public void test(){
Outer$Inner oi = new Outer$Inner();
oi.innerMethod();
}
static int access$0(){
return shared;
}
}
public class Outer$Inner(){
public void innerMethod(){
print("inner"+Outer.access$0());
}
}
内部类访问了外部类的一个私有静态属性shared
,而我们知道私有属性是不能被外部类访问的,Java的解决办法是:自动为Outer生成一个非私有访问方法access$0,返回私有变量shared。
静态内部类的使用场景是非常多的,如果它与外部类关系密切,且不依赖于外部类实例,则可以考虑定义为静态静态内部类。比如,一个类,如果既要计算最大值,又要计算最小值,可以在一次遍历中将最大值和最小值都计算出来,可以定义一个静态内部类Pair,包括最大值和最小值,而且它主要是在类内部使用,就可以定义为一个静态内部类。
Java API中使用静态内部类的例子:
- Integer中有一个私有静态内部类IntegerCache,用于支持整数的自动装箱以及缓存。
- 表示链表的LinkedList类内部有一个私有静态内部类Node,表示链表中的每一个节点。
- Character类内部有一个public静态内部类UnicodeBlock,用于表示一个Unicode Block。
成员内部类
与静态内部类相比,成员内部类没有static关键字。
public class Outer{
private int a = 100;
public class Inner{
public void innerMethod(){
print("outer a"+ a);
Outer.this.action();
}
}
private void action(){
print("action");
}
public void test(){
Inner inner = new Inenr();
inner.innerMethod();
}
}
其中,Inner就是成员内部类,与静态内部类不同,除了静态方法和属性,成员内部类还可以直接访问外部类的实例属性和方法,例如innerMethod直接访问外部类私有实例属性a
。成员内部类还可以通过外部类.this.xxx
访问外部类的实例属性和方法。如Outer.this.action()
,这种写法一般是重名的情况下,如果没有重名,那么外部类.this
是多余的。
在外部类内,使用成员内部类和静态内部类是一样的,直接使用即可,如test()
方法所示。与静态内部类不同,成员内部类对象总是与外部类对象相连的,在外部使用时,它不能直接通过new Outer.Inner
这种方式创建对象,而是要先创建一个Outer类实例,例:
Outer outer = new Outer():
Outer.Inner inner = outer.new Inner();
inner.innerMethod();
创建成员内部类的语法是外部类对象.new 成员内部类构造方法()
,如outer.new Inner()
。
与静态内部类不同,成员内部类不能定义静态属性和方法(final 属性除外),方法内部类和匿名内部类也不可以。这些内部类是与外部示例相连的,不应该独立使用,而静态属性和方法作为类型的属性和方法,一般是独立使用的,在内部类中意义不大,如果内部类确实需要静态属性和方法,那么可以定义写在外部类。
实际代码会生成两个类:一个是Outer,另一个是Outer$Inner,大概如下:
public class Outer{
private int a = 100;
private void action(){
print("action");
}
public void test(){
Outer$inner inner = new Outer$inenr(this);
inner.innerMethod();
}
static int access$0(Outer outer){
return outer.a;
}
static void access$1(Outer outer){
outer.action();
}
}
public class Outer$Inner(){
private final Outer outer;
public Outer$Inner(Outer outer){
this.outer = outer;
}
public void innerMethod(){
print("outer a"+Outer.access$0(outer));
Outer.access$1(outer);
}
}
OuterInner对象时传递当前对象,由于内部类访问外部类的私有属性和方法,外部类Outer生成了两个静态方法:$access0访问私有静态属性a,access$1用于访问私有实例方法action。
如果内部类与外部类关系密切,需要访问外部类的实例变量或方法,则可考虑定义为成员内部类。外部类的一些方法的返回值可能是某个接口,为了返回这个接口,外部类方法可以使用内部类实现这个接口,这个内部类可以被定义为private,对外完全隐藏。
比如,在Java API的类LinkedList中,它的两个方法listIterator和descendingIterator的返回值都是接口Iterator,调用者可以通过Iterator接口对链表进行遍历,listIterator和descendingIterator内部类分别使用了成员内部类ListItr和DescendingIterator,这个内部类都实现了接口Iterator。
方法内部类
内部类还可以定义在一个方法中。
public class Outer{
private int a = 100;
public void test(final int param){
final String msg = "hello";
Class Inner{
public void innerMethod(){
print("outer a"+a);
print("param"+param);
print("local variable"+msg);
}
}
Inner inner = new Inner();
inner.innerMethod();
}
}
类Inner定义在外部类实例方法test中,方法内部类只能在定义的方法内被使用。如果方法是实例方法,则除了静态变量和方法,内部类还可以直接访问实例变量和方法,如innerMethod
直接访问了私有实例变量a。如果是静态方法,则方法内部类只能访问外部类静态属性和方法。方法内部类还可以直接访问方法的参数和方法中的局部变量,不过,方法参数和局部变量必须被声明为final,如innerMethod访问了方法参数param和局部变量msg。
实际代码会生成两个类,大概如下:
public class Outer{
private int a = 100;
public void test(final int param){
final String msg = "hello";
OuterInner inner = new OuterInner(this,param);
inner.innerMethod();
}
static int access$0(){
return a;
}
}
public class Outer$Inner(){
Outer outer;
int param;
Outer$Inner(Outer outer, int param){
this.outer = outer;
this.param = param;
}
public void innerMethod(){
print("outer a"+outer.a);
print("param"+param);
print("local variable"+"hello");
}
}
与成员内部类类似,Outer$Inner也有一个实例实例属性指向Outer对象,在构造方法中被初始化,对外部私有成员属性也是通过Outer添加的方法access$0来进行的。
方法内部类可访问方法中的参数和局部变量,这是通过在构造方法中传递参数实现的,如OuterInner对象时,Outer类将方法中的参数传递给了内部类,如Outer$Inner inner = new Outer$Inner(this,param);
。在上面代码中,Sting msg并没有被作为参数传递,这是因为它被定义为了常量,在生成的代码中,可以直接使用它的值。
这样解释了为什么方法内部类访问外部方法中的参数和局部变量时,这些变量必须声明为final
,因为实际上,方法内部类操作的并不是外部的变量,而是自己的实例属性,只是这些变量的值和外部一样,对这些变量赋值,并不会改变外部的值,为避免混淆,所以干脆强制规定必须声明为final。
如果确实要修改外部的变量,那么可以将变量改为只包含该变量的数组,修改数组中的值,例:
public class Outer{
public void test(){
final String[] msgArr = new String[]{"hello"};
class Inner{
public void innerMethod(){
str[0] = "world";
}
}
Inner inner = new Inner();
inner.innerMethod();
print(msgArr[0]);
}
}
msgArr是一个只包含一个元素的数组,方法内部类不能msgArr本身的引用,但可以修改元素的值。通过上面可以看出方法内部类可以代替成员内部类,至于方法参数,也可以作为参数传递给成员内部类。不过,如果类只在某个方法内被使用,使用方法内部类,可以实现更好的封装。
匿名内部类
匿名内部类没有单独的定义,它在创建对象的同时定义类,语法如下: