6.6.5 接口和抽象类

接口和抽象类的相同点

  • 接口和抽象类都不能被实例化,它们都位于继承树的顶端,用于被其他类实现和继承。
  • 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法

接口和抽象的的区别

接口和抽象类再设计目的上的区别

但接口和抽象类之间的差别非常大,这种差别主要体现在二者设计目的上。下面具体分析二者的差别

使用接口的目的

接口作为系统与外界交互的窗口,接口体现的是一种规范

  • 对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供);
  • 对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。
  • 当在一个程序中使用接口时,接口是多个模块间的耦合标准;
  • 当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。

从某种程度上来看,接口类似于整个系统的”总纲”,它制定了系统各模块应该遵循的标准,因此个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。

使用抽象类的目的

抽象类则不一样,抽象类作为系统中多个子类的共同父类,它所体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。

接口和抽象类再用法上的区别

除此之外,接口和抽象类在用法上也存在如下差别。

  1. 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法
  2. 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
  3. 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  4. 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
  5. 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足。

6.6.4 使用接口

实现类赋值给接口引用变量

接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。

接口用途

归纳起来,接口主要有如下用途

  1. 定义变量,也可用于进行强制类型转换。
  2. 调用接口中定义的常量。
  3. 被其他类实现。

实现接口可以获得所实现接口里定义的常量(成员变量)、方法(包括抽象方法和默认方法)

implements必须放在extends之后

一个类可以继承一个父类,并同时实现多个接口, implements部分必须放在extends部分之后

非抽象类必须全部实现接口中定义的抽象方法

一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);

抽象类可以不重写接口抽象方法

抽象类可以保留从父接口那里继承到的抽象方法。

实现接口方法是只能使用public访问控制修饰符

实现接口方法时,必须使用public访问控制修饰符,因为接口里的方法都是public的,而子类(相当于实现类)重写父类方法时访问权限只能更大或者相等,所以实现类实现接口里的方法时只能使用public访问权限。

接口不能显示继承任何类

接口不能显式继承任何类,但所有接口类型的引用变量都可以直接赋给Object类型的引用变量。

6.6.3 接口的继承

接口完全支持多继承,即一个接口可以有多个直接父接口,子接口扩展某个父接口,则**子接口将会获得父接口里定义的所有抽象方法常量**。
一个接口继承多个父接口时,多个父接口排在extends关键字之后,多个父接口之间以英文逗号(,)隔开。

6.6.2 Java9中接口的定义

定义接口使用interface关键字。接口定义的基本语法如下:

1
2
3
4
5
6
7
[修饰符] interface 接口名 extends 父接口1,父接口2
{
零个到多个常量定义,
零个到多个抽象方法定义
零个到多个内部类、接口、枚举定义
零个到多个私有方法、默认方法或类方法定义
}

对上面语法的详细说明如下:

  • 接口的修饰符可以是public或者省略,如果省略了public访问控制符,则默认釆用包权限访问控制符,即只有在相同包结构下才可以访问该接口。
  • 接口名应与类名采用相同的命名规则,如果要遵守Java可读性规范,则接口名应由多个有意义的单词连缀而成,每个单词首字母大写,单词与单词之间无须任何分隔符。接口名通常能够使用形容词。
  • 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

在上面语法定义中,只有在Java8以上的版本中才允许在接口中定义默认方法、类方法

接口中 不能有构造器 不能有初始化块

由于接口定义的是一种规范,因此接口里不能包含构造器初始化块定义

接口里可以有的成员

接口里可以包含

  • 成员变量(只能是静态常量)、
  • 方法(只能是共有抽象实例方法类方法默认方法私有方法)、
  • 内部类(包括内部接口、枚举)定义。

接口里的成员变量只能是静态常量,接口里的方法只能是抽象方法类方法默认方法私有方法这四种

接口里只能有public修饰符和默认控制符

定义接口成员时,可以省略访问控制修饰符,如果要指定访问控制修饰符,则只能使用public访问控制修饰符。

java9中为接口增加私有方法

Java9为接口增加了一种新的私有方法,

私有方法的主要作用就是作为工具方法,为接口中的默认方法或类方法提供支持。
私有方法可以拥有方法体,但私有方法不能使用default修饰。
私有方法可以使用static修饰,也就是说,**私有方法既可是类方法,也可是实例方法**。

接口里的成员变量都是静态常量

对于接口里定义的静态常量而言,它们是接口相关的,因此系统会自动为这些成员变量增加staticfinal两个修饰符。也就是说,在接口中定义成员变量时,不管是否使用public static final修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值

接口中的方法说明

接口里定义的方法只能是抽象方法类方法默认方法私有方法,

接口中的普通方法默认使用public abstract修饰

  • 定义接口里的普通方法时不管是否使用public abstract修饰符,接口里的普通方法总是使用public abstract来修饰。
  • 接口里的普通方法不能有方法实现(方法体);

接口中类方法 默认方法 私有方法 必须有方法体

类方法默认方法私有方法必须有方法体

接口里的内部类 内部接口 内部枚举默认public static修饰

接口里定义的内部类、内部接口、内部枚举默认都采用public static两个修饰符,不管定义时是否指定这两个修饰符,系统都会自动使用public static对它们进行修饰。

java8增强的接口

Java8接口中的默认方法必须使用default修饰符

Java8允许在接口中定义默认方法,默认方法必须使用default修饰,该方法不能使用static修饰。

默认方法总是使用public修饰

默认方法总是使用public修饰,如果开发者没有指定public,系统会自动为默认方法添加public修饰符。

没有接口中静态默认方法

由于默认方法并没有static修饰,因此不能直接使用接口来调用默认方法,需要使用接口的实现类的实例来调用这些默认方法。

java8默认方法的实质

接口的默认方法其实就是实例方法,但由于早期Java的设计是:接口中的实例方法不能有方法体;Java8也不能直接”推倒”以前的规则,因此只好重定义一个所谓的”默方法”,默认方法就是有方法体的实例方法

java8可以在接口中定义静态方法

Java8允许在接口中定义类方法,类方法必须使用static修饰,该方法不能使用default修饰,无论程序是否指定,静态方法总是使用public修饰——如果开发者没有指定public,系统会自动为静态方法添加public修饰符。接口的静态方法可以直接使用接口来调用

java8增加了带方法体的私有方法

Java9增加了带方法体的私有方法,这也是Java8埋下的伏笔:Java8允许在接口中定义带方法体的默认方法和类方法——这样势必会引发一个问题,当两个默认方法(或类方法)中包含一段相同的实现逻辑时,程序必然考虑将这段实现逻辑抽取成工具方法,而工具方法是应该被隐藏的,这就是Java9增加私有方法的必然性。

接口里的成员变量默认public static final修饰

接口里的成员变量默认是使用public static final修饰的,因此即使另一个类处于不同包下,也可以通过接口来访问接口里的成员变量

public接口名必须和java源文件名相同

一个Java源文件里最多只能有个public接口,如果一个Java源文件里定义了一个public接口,则该源文件的主文件名必须与该接口名相同

6.6 Java9改进的接口

Java9对接口进行了改进,

  • java9允许在接口中定义默认方法类方法,默认方法和类方法都可以提供方法实现,
  • Java9为接口增加了一种私有方法,私有方法也可提供方法实现

6.6.1 接口的概念

接口定义的是多个类共同的公共行为规范,这些行为是与外部交流的通道,这就意味着接口里通常是定义一组公用方法

6.5.2 抽象类的作用

抽象类不能创建实例,只能当成父类来被继承。从语义的角度来看,抽象类是从多个具体类中抽象出来的父类,它具有更高层次的抽象。从多个具有相同特征的类中抽象出个抽象类,以这个抽象类作为其子类的模板,从而避免了子类设计的随意性。

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会大致保留抽象类的行为方式。

模板设计模式

如果编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,这就是一种模板模式,模板模式也是十分常见且简单的设计模式之一。
下面介绍一个模板模式的范例,在这个范例的抽象父类中,父类的普通方法依赖于一个抽象方法,而抽象方法则推迟到子类中提供实现。

模板模式使用规则

模板模式在面向对象的软件中很常用,其原理简单,实现也很简单。下面是使用模板模式的一些简单规则。

  1. 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
  2. 父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于其子类的辅助

6.5 抽象类

什么是抽象方法

抽象方法是只有方法签名,没有方法实现的方法

6.5.1 抽象方法和抽象类

抽象方法和抽象类必须使用abstract修饰符来定义,有抽象方法的类只能被定义成抽象类,抽象类里可以没有抽象方法

抽象方法规则

  • 抽象方法也使用abstract修饰符来修饰,
  • 抽象方法不能有方法体

定义抽象方法只需在普通方法上增加abstract修饰符,并把普通方法的方法体全部去掉,并在方法后增加分号即可。

抽象类规则

  • 抽象类必须使用abstract修饰符来修饰。
  • 抽象类不能被实例化,无法使用new关键字来调用抽象类的构造器创建抽象类的实例。即使抽象类里不包含抽象方法,这个抽象类也不能创建实例。
  • 抽象类可以包含成员变量方法构造器初始化块内部类5种成分。
    • 抽象类中的方法可以是普通方法也可以是抽象方法
    • 抽象类中的内部类包括接口枚举
    • 抽象类的构造器不能用于创建实例,主要是用于给其子类调用
  • 含有抽象方法的类只能被定义成抽象类,这包括以下三种情况
    1. 该类直接定义了一个抽象方法;
    2. 该类继承了一个抽象父类,但没有完全实现父类包含的抽象方法;
    3. 该类实现了一个接口,但没有完全实现接口包含的抽象方法。

抽象方法和空方法的区别

抽象方法和空方法体的方法不是同一个概念。例如,

  • public abstract void test();是一个抽象方法,它根本没有方法体,即方法定义后面没有一对花括号;
  • public void test(){}方法是一个普通方法,它已经定义了方法体(花括号),只是方法体为空,即它的方法体什么也不做,因此这个方法不可使用abstract来修饰。

小结

  • 空方法有花括号,花括号为空,
  • 抽象方法没有花括号

普通类前面加上abstract就可以变成抽象类

普通类

指的是没有包含抽象方法的类.
定义抽象类只需在普通类上增加abstract修饰符即可。甚至一个(没有包含抽象方法的)普通类增加abstract修饰符后也将变成抽象类

不能abstract一起使用的修饰符

final和abstract永远不能同时使用

  • 当使用abstract修饰类时,表明这个类只能被继承;
  • 当使用abstract修饰方法时,表明这个方法必须由子类提供实现(即重写)。
  • final修饰的类不能被继承,
  • final修饰的方法不能被重写。

因此**finalabstract永远不能同时使用**。

没有抽象变量 没有抽象成员变量

abstract不能用于修饰成员变量,
abstract不能用于修饰局部变量,
即没有抽象变量、没有抽象成员变量等说法;

没有抽象构造器

abstract也不能用于修饰构造器,没有抽象构造器,抽象类里定义的构造器只能是普通构造器。

没有静态抽象方法

当使用static修饰一个方法时,表明这个方法属于该类本身,即通过类就可调用该方法。
如果该方法被定义成抽象方法,由于抽象方法没有方法体,所以通过该类来调用该方法时会出现错误。因此staticabstract不能同时修饰某个方法,即没有所谓的抽象方法

staticabstract并不是绝对互斥的, staticabstract虽然不能同时修饰某个方法,但**staticabstract可以同时修饰内部类**

没有私有抽象方法

abstract关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此abstract方法不能定义为private访问权限,即**privateabstract不能同时修饰方法**

6.4.8 缓存实例的不可变类

缓存不可变类的实例的意义

不可变类的实例状态不可改变,可以很方便地被多个对象所共享。如果程序经常需要使用相同的不可变类实例,则应该考虑缓存这种不可变类的实例。毕竟重复创建相同的对象没有太大的意义,而且加大系统开销。如果可能,应该将已经创建的不可变类的实例进行缓存。

什么时候应该缓存

如果某个对象只使用一次,重复使用的概率不大,缓存该实例就弊大于利;反之,如果某个对象需要频繁地重复使用,缓存该实例就利大于弊。

Integer中的缓存

Java提供的java.lang.Integer类就采用了缓存,

  • 如果采用new构造器来创建Integer对象,则每次返回全新的Integer对象;
  • 如果采用valueOf()方法来创建Integer对象,则会缓存该方法创建的对象。

推荐使用valueOf方法创建Integer实例

  • 由于通过new构造器创建Integer对象不会启用缓存,因此性能较差,Java9已经将该构造器标记为过时.
  • 所以推荐是用valueOf()方法创建Integer对象

Integer缓存的范围

由于Integer只缓存负128到正127之间的Integer对象因此两次通过Integer.valueOf(200);方法生成的Integer对象不是同一个对象

实例 自己实现一个缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class CacheImmutale {
private static int MAX_SIZE = 10;
// 使用数组来缓存已有的实例
private static CacheImmutale[] cache = new CacheImmutale[MAX_SIZE];
// 记录缓存实例在缓存中的位置,cache[pos-1]是最新缓存的实例
private static int pos = 0;
private final String name;

private CacheImmutale(String name) {
this.name = name;
}

public String getName() {
return name;
}

public static CacheImmutale valueOf(String name) {
// 遍历已缓存的对象,
for (int i = 0; i < MAX_SIZE; i++) {
// 如果已有相同实例,直接返回该缓存的实例
if (cache[i] != null && cache[i].getName().equals(name)) {
return cache[i];
}
}
// 如果缓存池已满
if (pos == MAX_SIZE) {
// 把缓存的第一个对象覆盖,即把刚刚生成的对象放在缓存池的最开始位置。
cache[0] = new CacheImmutale(name);
// 把pos设为1
pos = 1;
} else {
// 把新创建的对象缓存起来,pos加1
cache[pos++] = new CacheImmutale(name);
}
return cache[pos - 1];
}

public boolean equals(Object obj) {
// 如果是同一个对象的引用,自然相等
if (this == obj) {
return true;
}
// 如果类型相同
if (obj != null && obj.getClass() == CacheImmutale.class) {
// 类型相同,可以强制类型转换
CacheImmutale ci = (CacheImmutale) obj;
// 比较值
return name.equals(ci.getName());
}
return false;
}

public int hashCode() {
return name.hashCode();
}
}

public class CacheImmutaleTest {
public static void main(String[] args) {
CacheImmutale c1 = CacheImmutale.valueOf("hello");
CacheImmutale c2 = CacheImmutale.valueOf("hello");
// 因为会缓存,所以这两个是同一个对象的引用,返回true
System.out.println(c1 == c2);
}
}

6.4.7 不可变类immutable

什么是不可变类

不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的

Java中的不可变类

Java提供的8个包装类java.lang.String类都是不可变类,当创建它们的实例后,其实例的实例变量不可改变。

如何自定义不可变类

  1. 使用privatefinal修饰符来修饰该类的成员变量
  2. 提供带参数构造器,用于根据传入参数来初始化类里的成员变量。
  3. 仅为该类的成员变量提供getter方法,不要为该类的成员变量提供setter方法,因为普通方法无法修改final修饰的成员变量
  4. 如果有必要,重写Object类的hashCode()equals()方法.
    • equals()方法根据关键成员变量来作为两个对象是否相等的标准,
    • 还应该保证两个用equals方法判断为相等的对象的hashCode也相等。

什么是可变类

可变类的含义是该类的实例变量是可变的。大部分时候所创建的类都是可变类,特别是JavaBean,因为总是为其实例变量提供了settergetter方法。

成员变量类型是可变类的情况

当使用final修饰引用类型变量时,仅表示这个引用类型变量不可被重新赋值,但引用类型变量所指向的对象的成员变量值依然可改变。这就产生了一个问题:当创建不可变类时,如果它包含成员变量的类型是可变的,那么其对象的成员变量的值依然是可改变的,则这个类一人不是不可变类。

如果需要设计一个不可变类,尤其要注意其引用类型的成员变量,如果引用类型的成员变量的类是可变的,则要想办法让外界无法正确获取成员变量的引用.

当成员变量的类型是可变的的时候如何设计不可变类

  1. 在构造器中不要将可变类的参数直接赋值给成员变量,而是取出参数的值,然后再封装成新对象,再将这个新对象赋值给成员变量.
  2. getter方法中也不要直接返回成员变量对象,而是取出成员变量对象中的值,再封装成新的一个对象返回.

这样外界无法获取成员变量的引用,从而无法修改成员变量对象的值.从而保证这个类是真正的不可变类.

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 这是一个可变类
class Name {
private String firstName;
private String lastName;

public Name() {
}

public Name(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

// 省略firstName、lastName的setter和getter方法
public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getFirstName() {
return this.firstName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getLastName() {
return this.lastName;
}
}

public class Person {
// final修饰的成员变量不可以赋值新对象,但是对象里面的值可以修改
private final Name name;

public Person(Name name) {
// 成员变量和name引用的不是同一个对象
// 外部的name引用变量无法修改成员变量对象的值
this.name = new Name(name.getFirstName(), name.getLastName());
}

/**
* 不直接返回成员变量,而是返回可变成员变量的一个副本,避免被外界修改
*
* @return
*/
public Name getName() {
// 返回的是新的对象,外界的引用依然无法修改成员变量的值
return new Name(name.getFirstName(), name.getLastName());
}

public static void main(String[] args) {
Name n = new Name("悟空", "孙");
Person p = new Person(n);
// 获取的永远是副本
System.out.println(p.getName().getFirstName());
// 无法修改到p对象中的成员变量
n.setFirstName("八戒");
// 获取的永远是副本
System.out.println(p.getName().getFirstName());
}
}

运行结果:

1
2
悟空
悟空