继承和多态

Table of Contents

继承机制的使用可以复用一些定义好的类,减少重复代码的编写。多态机制可以动态调整对象的调用,降低对象之间的依存关系。

封装

封装将类的某个信息隐藏在类内部,不允许外部程序直接访问,只能通过该类提供的方法来实现对隐藏信息的操作和访问。

例如:一台计算机内部极其复杂,有主板、CPU、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU 主频、硬盘和内存的大小,于是计算机制造商将用机箱把计算机封装起来,对外提供了一些接口,如鼠标、键盘和显示器等,方便用户使用计算机。

封装的特点:

  • 只能通过规定的方法访问数据;
  • 隐藏类的实例细节,方便修改和实现。

实现封装的具体步骤如下:

  1. 修改属性的可见性来限制对属性的访问,一般设为 private
  2. 为每个属性创建一对赋值( setter )方法和取值( getter )方法,一般设为 public ,用于属性的读写;
  3. 在赋值和取值方法中,加入属性控制语句(对属性值的合法性进行判断)。

下面我们来看一个完整的例子,要求编写表示图书的 Book 类,实现以下需求:

  • 基本信息包括图书名称( bookName )、总页数( bookTotalNum ),其中页数不能少于 200 页,否则输出错误信息,并赋予默认值 200 ;
  • 为各个属性设置赋值和取值方法;
  • 具有 details() 方法,该方法在控制台输出每本图书的名称和总页数。

编写 BookTest 测试类,为 Book 对象的属性赋予初始值,并调用 details() 方法输出详细信息。

 1: public class Book {
 2:     private String bookName;
 3:     private int bookTotalNum;
 4: 
 5:     public String getBookName() {
 6:         return bookName;
 7:     }
 8:     public void setBookName(String bookName) {
 9:         this.bookName = bookName;
10:     }
11: 
12:     public int getBookTotalNum() {
13:         return bookTotalNum
14:     }
15:     public void setBookTotalNum(int bookTotalNum) {
16:         if (bookTotalNum < 200) {
17:             System.out.println(this.bookName + "这本书的页数不能少于 200 页");
18:             this.bookTotalNum = 200;
19:         } else {
20:             this.bookTotalNum = bookTotalNum;
21:         }
22:     }
23: 
24:     public void details() {
25:         System.out.println(this.bookName + "这本书的总页数是:" + this.bookTotalNum);
26:     }
27: }

测试类如下:

 1: public class BookTest {
 2:     public static void main(String[] args) {
 3:         Book book1 = new Book();
 4:         book1.setBookName("《红与黑》");
 5:         book1.setBookTotalNum(190);
 6:         book1.details();
 7:         System.out.println("**********************");
 8:         Book book2 = new Book();
 9:         book2.setBookName("《格林童话》");
10:         book2.setBookTotalNum(520);
11:         book2.details();
12:     }
13: }

代码执行结果如下:

《红与黑》这本书的页数不能少于 200 页
《红与黑》这本书的总页数是:200
************************************
《格林童话》这本书的总页数是:520

继承

Java 中的继承就是在已经存在类的基础上进行扩展,从而产生新的类。已经存在的类称为 父类、基类或超类 ,而新产生的类称为 子类或派生类 。在子类中,不仅包含父类的属性和方法,还可以增加新的属性和方法。

Java 中子类继承父类的语法格式如下:

修饰符 class class_name extends extend_class {
       //  类的主体
}

其中, class_name 表示子类(派生类)的名称; extend_class 表示父类(基类)的名称; extends 关键字直接跟在子类名之后,其后面是该类要继承的父类的名称。

Java 与 C++ 定义继承类的方式十分相似。Java 用关键字 extends 代替了 C++ 中的冒号( )。在 Java 中,所有的继承都是公有继承, 而没有 C++ 中的私有继承和保护继承。

类的继承不改变类成员的访问权限,也就是说,如果父类的成员是公有的、被保护的或默认的,它的子类仍具有相应的这些特性,并且子类不能获得父类的构造方法。

*注:如果父类中存在有参的构造方法而并没有重载无参的构造方法,那么在子类中必须含有有参的构造方法。因为如果在子类中不含有构造方法,默认会调用父类中无参的构造方法,而在父类中并没有无参的构造方法,因此会出错。

单继承

Java 语言摒弃了 C++ 中难以理解的多继承特征,即 Java 不支持多继承,只允许一个类直接继承另一个类,即子类只能有一个直接父类, extends 关键字后面只能有一个类名。

如果定义一个 Java 类时并未显示指定这个类的直接父类,则这个类默认继承 java.lang.Object 类。

使用继承的注意点:

  • 子类一般比父类包含更多的属性和方法;
  • 父类中的 private 成员在子类中是不可见的,因此在子类中不能直接使用它们;
  • 父类和其子类间必须存在“是一个”即 is-a 的关系,否则不能用继承;
  • Java 只允许单一继承。

在面向对象语言中,继承是必不可少的、非常优秀的语言机制,它有如下优点:

  • 实现代码共享,减少创建类的工作量,使子类可以拥有父类的方法和属性;
  • 提高代码维护性和可重用性;
  • 提高代码的可扩展性,更好的实现父类的方法。

所有事物有其好的一面,也有其不好的一面,继承的缺点如下:

  • 继承是侵入性的,只要继承,就必须拥有父类的属性和方法;
  • 降低代码灵活性,子类拥有父类的属性和方法后多了些约束;
  • 增强代码耦合性( 开发项目的原则为高内聚低耦合 ),当父类的常量、变量和方法被修改时,需要考虑子类的修改,有可能导致大段的代码重构。

super

由于子类不能继承父类的构造方法,因此,如果要调用父类的构造方法,可以使用 super 关键字。 super 可以用来访问父类的构造方法、普通方法和属性。

super 关键字的功能:

  • 在子类的构造方法中显示的调用父类构造方法;
  • 访问父类的成员方法和变量。

1. super 调用父类构造方法

super 关键字可以在子类的构造方法中显式地调用父类的构造方法,基本格式如下:

super(parameter-list);

其中, parameter-list 指定了父类构造方法中的所有参数。 super() 必须在子类构造方法的方法体的第一行。

我们来看一段代码,声明父类 Person 和子类 Student ,在 Person 类中定义一个带有参数的构造方法,如下:

1: public class Person {
2:     public Person(String name) {
3: 
4:     }
5: }
6: public class Student extends Person {
7: 
8: }

上述代码会出现 Student 类的编译错误,提示必须显式定义构造方法,错误信息如下:

Implicit super constructor Person() is undefined for default constructor. Must define an explicit constructor.

为什么会出现这个错误呢?

本例中,JVM 默认给 Student 类加了一个无参构造方法,而在这个方法中默认调用了 super() ,但是 Person 类中并不存在该构造方法,所以会编译错误。

*小结一下:
如果一个类中没有写任何的构造方法,JVM 会生成一个默认的无参构造方法。在继承关系中,由于在子类的构造方法中,第一条语句默认为调用父类的无参构造方法(即默认为 super() ,一般这行代码省略了)。所以当在父类中定义了有参构造方法,但是没有定义无参构造方法时,编译器会强制要求我们定义一个相同参数类型的构造方法。

2. super 访问父类成员

当子类的成员变量或方法与父类同名时,可以使用 super 关键字来访问。

如果子类重写了父类的某一个方法,即子类和父类有相同的方法定义,但是有不同的方法体,此时,我们可能通过 super 来调用父类里面的这个方法。

使用 super 访问父类中的成员与 this 关键字的使用类似,只不过它引用的是子类的父类 ,语法格式如下:

super.member

3. super 和 this 的区别

this 指的是当前对象的引用, super 是当前对象的父对象的引用。如果构造方法的第一行代码不是 this()super() ,则系统会默认添加 super()

关于 Java 中 superthis 关键字的异同,可简单总结为以下几条:

(1)子类和父类中变量或方法名称相同时,用 super 关键字来访问。可以理解为 super 是指向自己父类对象的一个指针,在子类中调用父类的构造方法。

(2) this 是自身的一个对象,代表对象本身,可以理解为 this 是指向对象本身的一个指针,在同一个类中调用其它方法。

(3) thissuper 不能同时出现在一个构造方法里面,因为 this 必然会调用其它的构造方法,其它构造方法中肯定会有 super 语句存在,所以在同一个构造方法里面有相同的语句,就失去了语句的意义,编译器也不会通过。

(4) this()super() 都指的是对象,所以,均不可以在 static 环境中使用,包括 static 变量、 static 方法和 static 语句块。

(5)从本质上讲, this 是一个指向对象本身的指针,然而 super 是一个 Java 关键字。

对象类型转型

将一个类型强制转换成另一个类型的过程被称为 类型转型 ,这里所说的 对象类型转型 ,是指 存在继承关系 的对象,不是任意类型的对象。当对不存在继承关系的对象进行强制类型转换时,会抛出 Java 强制类型转换( java.lang.ClassCastException )异常。

Java 语言允许某个类型的引用变量引用子类的实例,而且可以对这个变量进行转换。

Java 中引用类型之间的类型转换(前提是两个类是父子关系)主要有两种,分别的向上转型(upcasting)和向下转型(downcasting)。

(1)向上转型

父类引用指向子类对象为向上转型,语法格式如下:

fatherClass obj = new sonClass();

其中, fahterClass 是父类名称或接口名称, obj 是创建的对象, sonClass 是子类名称。

向上转型就是把子类对象赋给父类引用,不用强制转换。使用向上转型可以调用父类类型中的所有成员, 不能调用子类类型中特有成员 ,最终运行效果看子类的具体实现。

(2)向下转型

与向上转型相反,子类对象指向父类引用向下转型,语法格式如下:

sonClass obj = (sonClass) fahterClass;

向下转型可以调用子类类型中所有成员,不过需要注意的是:

如果父类引用对象指向的是子类对象,那么在向下转型的过程中是安全的,也就是编译是不会出错误。

但是如果父类引用对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错,但是运行时会出现我们开始提到的 Java 强制类型转换异常,一般使用 instanceof 运算符来避免出此类错误。

例如,Animal 类表示动物类,该类对应的子类有 Dog 类,使用对象类型表示如下:

1: Animal animal = new Dog();      // 向上转型,把 Dog 类型转换为 Animal 类型
2: Dog dog = (Dog) animal;         // 向下转型,把 Animal 类型转换为 Dog 类型

我们来看一个具体的示例演示对象类型的转换。例如,父类 Animal 和子类 Cat 中都定义了实例变量 name 、静态变量 staicName 、实例方法 eat() 和静态方法 staticEat() 。此外,子类 Cat 中还定义了实例变量 str 和实例方法 eatMethod()

父类 Animal 的代码如下:

 1: public class Animal {
 2:     public string name = "Animal: 动物";
 3:     public static String staticName = "Animal: 可爱的动物";
 4: 
 5:     public void eat() {
 6:         System.out.println("Animal: 吃饭");
 7:     }
 8: 
 9:     public static void staticEat() {
10:         System.out.println("Animal: 动物在吃饭");
11:     }
12: }

子类 Cat 的代码如下:

 1: public class Cat extends Animal {
 2:     public String name = "Cat: 猫";
 3:     public String str = "Cat: 可爱的小猫";
 4:     public static String staticName = "Dog: 我是喵星人";
 5: 
 6:     public void eat() {
 7:         System.out.println("Cat: 吃饭");
 8:     }
 9: 
10:     public static void staticEat() {
11:         System.out.println("Cat: 猫在吃饭");
12:     }
13: 
14:     public void eatMethod() {
15:         System.out.println("Cat: 猫喜欢吃鱼");
16:     }
17: 
18:     public static void main(String[] args) {
19:         Animal animal = new Cat();
20:         Cat cat = (Cat) animal;                // 向下转型
21: 
22:         System.out.println(animal.name);       // 输出 Animal 类的 name 变量 → Animal: 动物
23:         System.out.println(animal.staticName); // 输出 Animal 类的 staticName 变量 → Animal: 可爱的动物
24: 
25:         animal.eat();                          // 输出 Cat 类的 eat() 方法 → Cat: 吃饭
26:         animal.staticEat();                    // 输出 Animal 类的 staticEat() 方法 → Cat: 动物在吃饭
27: 
28:         System.out.println(cat.str);           // → 调用 Cat 类的 str 变量 → Cat: 可爱的小猫
29:         cat.eatMethod();                       // → 调用 Cat 类的 eatMethod 变量 → Cat: 猫喜欢吃鱼
30:     }
31: }

通过引用类型变量来访问所引用的属性和方法时,Java 虚拟机将采用以下绑定规则:

  • 实例方法与引用变量实际引用的对象的方法进行绑定,这种绑定属于 动态绑定 ,因为是在 运行时 由 Java 虚拟机动态决定的。例如, animal.eat() 是将 eat() 方法与 Cat 类相绑定;
  • 静态方法与引用变量所声明的类型的方法绑定,这种绑定属于静态绑定,因为是在 编译阶段 已经做了绑定。例如, animal.staticEat() 是将 staticEat() 方法与 Animal 类进行绑定;
  • 成员变量(包括静态变量和实例变量)与引用变量所声明的成员变量绑定,这种绑定属于静态绑定,因为在编译阶段已经做了绑定。例如, animal.nameanimal.staticName 都是与 Animal 类进行绑定。

方法重载

Java 允许同一个类中定义多个同名方法,只要它们的形参列表不同即可。

如果同一个类中包含了两个或两个以上方法名相同,但形参列表不同的方法,这种情况称为 方法重载 (overload)。

如:在 JDK 的 java.io.PrintStream 中定义了十多个同名的 println() 方法。

1: public void println(int i) { ... }
2: public void println(double d) { ... }
3: public void println(String s) { ... }

这些方法完成的功能类似,都是格式化输出。根据参数的不同来区分它们,以进行不同的格式化处理和输出。它们之间就构成了方法的重载。实际调用时,根据实参的类型来决定调用哪一个方法。

*注:方法重载的要求是两同一不同:同一个类中方法名相同,参数列表不同。至于方法的其他部分,如方法返回值、修饰符等,与方法重载没有任何关系。

使用方法重载其实就是避免出现繁多的方法名,有些方法的功能是相似的,如果重新建立一个方法,重新取个方法名称,会降低程序可读性。

方法重载,其实就是同类同名不同参啦,别的木有什么要求。

方法重写

在子类中如果创建一个与父类中相同名称、相同返回值类型、相同参数列表的方法,只是方法体中的实现不同,以实现不同于父类的功能,这种方式被称为 方法重写 (override),又称为方法覆盖。

当父类中的方法无法满足子类需求或子类具有特有功能的时候,就需要方法重写。子类可以根据需要,定义特定于自己的行为。既沿袭了父类的功能名称,又根据子类的需要重新实现父类方法,从而进行扩展增强。

在重写方法时,需要遵循下面的规则:

  • 参数列表必须完全与被重写的方法参数列表相同;
  • 返回的类型必须与重写的方法的返回类型相同(Java 1.5 之后放宽了限制,返回值类型必须小于或等于父类方法的返回值类型);
  • 访问权限不能比父类中被重写方法的访问权限更低(public > protected > default > private);
  • 重写方法一定不能抛出新的检查异常或者比被重写方法声明更加宽泛的检查型异常。例如,父类的一个方法声明了一个检查异常 IOException ,在重写这个方法时就不能抛出 Exception ,只能抛出 IOExeption 的子类异常,可以抛出非检查异常。

另外还要注意以下几条:

  • 重写的方法可以使用 @Override 注解来标识;
  • 父类的成员方法只能被它的子类重写;
  • 声明为 final 的方法不能被重写;
  • 声明为 static 的方法不能被重写,但是能够再再次声明;
  • 构造方法不能被重写;
  • 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为 privatefinal 的方法;
  • 子类和父类不在同一个包中时,子类只能重写父类的声明为 publicprotected 的非 final 方法;
  • 如果不能继承一个方法,则不能重写这个方法。

如果子类中创建了一个成员变量,而该变量的类型和名称都与父类中的同名成员变量相同,我们则称作变量隐藏。

多态

多态性是面向对象编程的又一个重要特征,它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

对面向对象来说,多态分为编译时多态和运行时多态。

其中:

  • 编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法。通过编译之后会变成两个不同的方法,在运行时谈不上多态;
  • 运行时多态是动态的,它是通过动态绑定来实现的,也就是大家通常所说的多态性。

Java 实现多态有 3 个必要的条件:继承、重写和向上转型。

  • 继承:在多态中必须存在有继承关系的子类和父类;
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时变会调用子类的方法;
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这们该引用才既能可以调用父类的方法,又能调用子类的方法。

只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

#. intanceof 关键字

在 Java 中可以用 instanceof 关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例,语法格式如下:

boolean result = obj instanceof Class

其中, obj 是一个对象, Class 表示一个类或接口, obj 是 class 类(或接口)的实例或者子类实例时,结果 result 返回 true ,否则返回 false

下面介绍 Java instanceof 关键字的几种用法。

(1)声明一个 class 类的对象,判断 obj 是否为 class 类的实例对象(很普遍的一种用法),如下:

1: Integer integer = new Integer(1);
2: System.out.println(integer instanceof Integer); // true

(2)声明一个 class 接口实现类的对象 obj ,判断 obj 是否为 class 接口实现类的实例对象,如下:

Java 集合中的 List 接口有个典型实现类 ArrayList

1: public class ArrayList<E> extends AbstractList<E>
2:     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

所以我们可以用 instanceof 运算符判断 ArrayList 类的对象是否属于 List 接口的实例,如果是返回 true ,否则返回 false

1: ArrayList arrayList = new ArrayList();
2: System.out.println(arrayList instanceof List); // true
3: // 反过来也是 true
4: List list = new ArrayList();
5: System.out.println(list instanceof ArrayList); // true

(3)obj 是 class 类的直接或间接子类

假设 Man 类是 Person 类的子类,代码如下:

1: Person p1 = new Person();
2: Person p2 = new Man();
3: Man m1 = new Man();
4: System.out.println(p1 instanceof Man); // → false
5: System.out.println(p2 instanceof Man); // → true
6: System.out.println(m1 instanceof Man); // → true

其中第 4 行,Man 是 Person 的子类,Person 不是 Man 的子类,所以返回结果为 false

*注: obj 必须为引用类型,不能是基本类型。

1: int i = 0;
2: System.out.println(i instanceof Integer); // 编译不通过
3: System.out.println(i instanceof Object);  // 编译不通过

所以, instanceof 运算符只能用作对象的判断。

objnull 时,直接返回 false ,因为 null 没有引用任何对象,所以 obj 的类型必须是引用类型或空类型,否则会编译错误。

另外,当 classnull 时,会发生编译错误,如下:

Syntax error on token "null", invalid ReferenceType

所以, class 只能是类或者接口。

编译器会检查 obj 能否转换成右边的 class 类型,如果不能转换则直接报错,如果不能确定类型,则通过编译。来看个例子:

1: Person p1 = new Person();
2: System.out.println(p1 instanceof String);       // 编译报错
3: System.out.println(p1 instanceof List);         // false
4: System.out.println(p1 instanceof List<?>);      // false
5: System.out.println(p1 instanceof List<Person>); // 编译错误

上述代码中, Person 的对象 p1 很明显不能转换为 String 对象,那么 p1 instanceof String 不能通过编译,但 p1 instanceof List 却能通过编译,而 instanceof List<Person> 又不能通过编译了。

为什么呢?

 1: boolean result;
 2: if (obj == null) {
 3:     result = false;             // 当 obj 为 null 时,直接返回 false
 4: } else {
 5:     try {
 6:         // 判断 obj 是否可以强制转换为 T
 7:         T temp = (T) obj;
 8:         result = true;
 9:     } catch (ClassCastException e) {
10:         result = false;
11:     }
12: }

可见,在 T 不为 nullobj 不为 null 时,如果 obj 可以转换为 T 而不引发异常( ClassCastException ),则该表达式的结果为 true ,否则值为 false

由此,可见, p1 instanceof String 会报编译错误,就是因为 (String) p1 是不能通过编译的,而 (List) p1 可以通过编译。

抽象类

Java 语言提供了两种类,分别为具体类和抽象类。

在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,那么这样的类称为 抽象类

在 Java 中抽象类的语法格式如下:

<abstract> class <class_name> {
    <abstract> <type> <method_name> (parameter-list);
}

如果一个方法使用 abstract 来修饰,则说明该方法是 抽象方法 ,抽象方法只有声明没有实现。

*注:需要注意的是 abstract 关键字只能用于普通方法,不能用于 static 方法或者构造方法中。

因为,静态方法和构造方法都是要用来使用和实现的,抽象方法只有声明没有实现,肯定是不行的啊。

抽象方法的 3 个特征如下:

  1. 抽象方法没有方法体;
  2. 抽象方法必须存在于抽象类中;
  3. 子类重写父类时,必须重写父类的所有的抽象方法。

*注:在使用 abstract 关键字修饰抽象方法时不能使用 private 修饰,因为抽象方法必须被子类重写。

抽象类的定义和使用规则如下:

  • 抽象类和抽象方法都要使用 abstract 关键字声明;
  • 如果一个方法被声明为抽象的,那么这个类也必须声明为抽象的,而一个抽象类中,也可以包含具体方法;
  • 抽象类不能被实例化,也就是不能使用 new 关键字创建对象。

来看一个具体的例子吧。

不同几何图形的面积计算公式是不同的,但是它们具有的特性是相同的,都具有长和宽这两个属性,也都具有面积计算的方法。那么可以定义一个抽象类,在该抽象类中含有两个属性( widthheight )和一个抽象方法 area() ,具体步骤如下:

(1)首先创建一个表示图形的抽象类 Shape ,代码如下:

 1: public abstract class Shape {
 2:     public int width;             // 
 3:     public int height;            // 
 4: 
 5:     public Shape (int width, int height) {
 6:         this.width = width;
 7:         this.height = height;
 8:     }
 9: 
10:     public abstract double area(); // 定义抽象方法,计算面积
11: }

(2)定义一个正方形类,该类继承自形状类 Shape ,并重写了 area() 抽象方法,代码如下:

 1: public class Square extends Shape {
 2:     public Square (int width, int height) {
 3:         super(width, height);
 4:     }
 5: 
 6:     // 重写父类中的抽象方法,实现计算正方形面积的功能
 7:     @Override
 8:     public double area() {
 9:         return width * height;
10:     }
11: }

(3)定义一个三角形,也继承自形状类 Shape ,并重写父类中的抽象方法 area() ,代码如下:

 1: public class Triangle extends Shape {
 2:     public Triangle (int width, int height) {
 3:         super(width, height);
 4:     }
 5: 
 6:     // 重写父类中的抽象方法,实现计算三角形面积的功能
 7:     @Override
 8:     public double area() {
 9:         return 0.5 * width * height;
10:     }
11: }

(4)最后创建一个测试类,分别创建正方形类和三角形类的对象,并调用各类中的 area() 方法,打印出不同形状的几何图形的面积,代码如下:

1: public class ShapeTest {
2:     public static void main(String[] args) {
3:         Square square = new Square(5, 4);
4:         System.out.println("正方形的面积为:" + square.area());   // → 20.0
5:         Triangle triangle = new Triangle(2, 5);
6:         System.out.println("三角形的面积为:" + triangle.area()); // → 5.0
7:     }
8: }

在 Shape 类的最后定义了一个抽象方法 area() ,用来计算图形的面积。在这里,Shape 类只是定义了计算图形面积的方法,而对于如何计算并没有任何限制。也可以这样理解,抽象类 Shape 仅定义了子类的一般形式。

接口

抽象类是从多个类中抽象出来的模板,如果将这种抽象进行的更彻底,则可以提炼出一种更加特殊的“抽象类” – 接口 (Interface)。

接口是 Java 中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口没有执行体,是由全局常量和公共抽象方法所组成。

定义接口

Java 接口的定义方式与类基本相同,不过接口定义的关键字是 interface ,接口定义的语法格式如下:

[public] interface interface_name [extends interface1_name[, interface2_name,...]] {
    // 接口体,其中可以包含定义常量和声明方法
    [public] [static] [final] type constant_name = value;        //定义常量
    [public] [abstract] returnType method_name (parameter_list); //声明方法
}

下面对以上语法做一些说明:

  • public 表示接口的修饰符,当没有修饰符时,则使用默认的修饰符,此时该接口的访问权限仅局限于所属的包;
  • interface_name 表示接口的名称;
  • extents 表示接口的继承关系;
  • interface1_name 表示要继承的接口名称;
  • constant_name 表示变量名称,一般是 staticfinal 型的;
  • return_type 表示方法返回值的类型;
  • parameter_list 表示参数列表,在接口中的方法是没有方法体的。

*注:一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。

接口对于其声明、变量和方法都做了许多限制,这些限制作为 接口的特征 如下:

  • 具有 public 访问控制符的接口,允许任何类使用,否则其访问权限局限 于所属的包;
  • 方法的声明不需要其他修饰符,在接口中声明的方法,将隐式地声明为公有的( public )和抽象的( abstract );
  • 在 Java 接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为 public、staticfinal ,即常量,所以接口中定义的变量 必须初始化
  • 接口没有构造方法,不能被实例化。

*注:一个接口不能够实现另一个接口,只可以继承接口,子接口可以对父接口的方法和常量进行重写。

1: public interfact StudentInterface extends PeopleInterface {
2:     int age = 25;               // 常量 age 重写父类接口中的 age 常量
3:     void getInfo();             // 方法 getInfo() 重写父接口中的 getInfo() 方法
4: }

实现接口

接口的主要用途就是被实现类实现 ,一个类可以实现一个或多个接口,继承使用 extends 关键字,实现则使用 implements 关键字。一个类可以实现多个接口,类实现接口的语法格式如下:

<public> class <class_name> [extends superclass_name] [implements interface1_name[, interface2_name...]] {
    // 主体
}

实现接口需要注意以下几点:

  • 实现接口与继承父类相似,一样可以获得所实现接口里定义的常量和方法。如果一个类需要实现多个接口,则多个接口之间可以逗号分隔;
  • 一个类可以继承一个父类,并同时实现多个接口, implements 部分必须放在 extends 部分之后;
  • 一个类实现了一个或多个接口之后,这个类必须完全实现这些接口里所定义的全部抽象方法(也就是重写这些抽象方法);否则,该类将保留从父接口那里继承到的抽象方法,该类也必须定义成抽象类。

来看一个具体的例子吧。

在程序的开发中,需要完成两个数的求和运算和比较运算功能的类非常多,所以可以定义一个接口来将类似的功能组织在一起。

(1)创建一个名称为 IMath 的接口:

1: public interface IMath {
2:     public int sum();                // 完成两个数的相加
3:     public int maxNum(int a, int b); // 获取较大的数
4: }

(2)定义一个 MathClass 类并实现 IMath 接口:

 1: public class MathClass implements IMath {
 2:     private int num1;
 3:     private int num2;
 4:     public MathClass(int num1, int num2) {
 5:         // 构造方法
 6:         this.num1 = num1;
 7:         this.num2 = num2;
 8:     }
 9:     // 实现接口中的求和方法
10:     public int sum() {
11:         return num1 + num2;
12:     }
13:     // 实现接口中的获取较大数的方法
14:     public int maxNum(int a, intb) {
15:         if (a >= b) {
16:             return a;
17:         } else {
18:             return b;
19:         }
20:     }
21: }

在实现类中,所有的方法都使用了 public 访问修饰符声明。无论何时实现一个由接口定义的方法,它都必须实现为 public ,因为接口中的所有成员都显示声明为 public

(3)最后创建一个测试类 NumTest ,实例化接口的实现类 MathClass ,调用该类中的方法并输出结果:

1: public class NumTest {
2:     public static void main(String[] args) {
3:         MathClass calc = new MathClass(100, 300);
4:         System.out.println(calc.sum());            // → 400
5:         System.out.println(calc.maxNum(100, 300)); // → 300
6:     }
7: }

TODO 内部类

TODO 匿名类

匿名类是指没有类名的内部类,必须在创建时使用 new 语句来声明类,语法形式如下:

new <类或接口> () {
    // 类的主体
}

Lambda 表达式

(Java 8 新特性)Lambda 表达式是一个匿名函数,它允许把函数作为一个方法的参数。

来看一个具体的应用例子:

先定义一个计算数值的接口,代码如下:

1: // 可计算接口
2: public interface Calculable {
3:     // 计算两个 int 数值
4:     int calculateInt(int a, int b);
5: }

实现方法代码如下:

 1: public class Test {
 2:     /**
 3:        通过操作符,进行计算
 4: 
 5:        @param opr 操作符
 6:        @return 实现 Calculable 接口对象
 7:      */
 8:     public static Calculable calculate(char opr) {
 9:         Calculable result;
10:         if (opr == '+') {
11:             // 匿名内部类实现 Calculable 接口
12:             result = new Calculable() {
13:                 // 实现加法运算
14:                 @Override
15:                 public int calculateInt(int a, int b) {
16:                     return a + b;
17:                 }
18:             };
19:         } else {
20:             // 匿名内部类实现 Calculable 接口
21:             result = new Calculable() {
22:                 // 实现减法运算
23:                 @Override
24:                 public int calculateInt(int a, int b) {
25:                     return a - b;
26:                 }
27:             }
28:         }
29:         return result;
30:     }
31: }

方法 calculateopr 参数是运算符,返回值是实现 Calculable 接口对象。

 1: public static void main(String[] args) {
 2:     int n1 = 10;
 3:     int n2 = 5;
 4:     // 实现加法计算 Calculable 对象
 5:     Calculable f1 = calculate('+');
 6:     // 实现加法计算 Calculable 对象
 7:     Calculable f2 = calculate('-');
 8:     // 调用 calculateInt 方法进行加法计算
 9:     System.out.println(f1.calculateInt(n1, n2)); // → 15
10:     // 调用 calculateInt 方法进行减法运算
11:     System.out.println(f2.calculateInt(n1, n2)); // → 5
12: }

不难看出,使用匿名内部类的方法 calculate 代码很臃肿 ,Java 8 采用 Lambda 表达式可以替代匿名内部类,修改之后的通用方法 calculate 代码如下:

 1: /**
 2:    通过操作符,进行计算
 3:    @param opr 操作符
 4:    @return 实现 Calculable 接口对象
 5:  */
 6: public static Calculable calculate(char opr) {
 7:     Calculable result;
 8:     if (opr == '+') {
 9:         // Lambda 表达式实现 Calculable 接口
10:         result = (int a, int b) -> {
11:             return a + b;
12:         }
13:     } else {
14:         // Lambda 表达式实现 Calculable 接口
15:         result = (int a, int b) -> {
16:             return a - b;
17:         }
18:     }
19:     return result;
20: }

第 10 行和第 15 行用 Lambda 表达式替代匿名内部类,代码变得简洁多了。

通过以上示例我们发现,Lambda 表达式是一个匿名函数(方法)代码块,可以作为表达式、方法参数和方法返回值类型。

Lambda 表达式标准语法形式如下:

(参数列表) -> {
    // Lambda 表达式
}

Java Lambda 表达式的优点:

  • 代码简洁,开发迅速;
  • 方便函数式编程;
  • 非常容易进行并行计算;
  • Java 引入 Lambda ,改善了八集合操作(引入 Stream API)。

当然也有缺点:

  • 代码可读性变差;
  • 在非并行计算中,很多计算未必有传统的 for 性能要高;
  • 不容易进行调试。

1. 函数式接口

Lambda 表达式实现的接口不是普通的接口,而是函数式接口。

如果一个接口中,有且只有一个抽象的方法(Oject 类中的方法不包括在内),那这个接口就可以被看做是 函数式接口 。这种接口只能有一个方法,如果接口中声明多个抽象方法,那么 Lambda 表达式会发生编译错误。

The target type of this expression must be a functional interface

说明该接口不是函数式接口,为了防止在函数式接口中声明多个抽象方法,Java 8 提供了一个声明函数式接口注解 @FunctionalInterface ,示例代码如下:

1: // 可计算接口
2: @FunctionalInterface
3: public interface Calculable {
4:     // 计算两个 int 数值
5:     int calculateInt(int a, int b);
6: }

在接口之前使用 @FunctionalInterface 注解修饰,那么试图增加一个抽象方法时会发生编译错误,但可以添加默认方法和静态方法。

@FunctionalInterface 注解与 @Override 注解的作用类似。Java 8 中专门为函数式接口引入了一个新的注解 @FunctionalInterface 。该注解可用于一个接口的定义上,一旦使用该注解来定义接口,编译器将会强制检查接口是否确实有且仅有一个抽象方法,否则将会报错。

*注:即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

提示:Lambda 表达式是一个匿名方法代码,Java 中的方法必须声明在类或接口中,那么 Lambda 表达式所实现的匿名方法是在函数式接口中声明的。

2. Lambda 表达式的使用

TODO…

Date: 2020-09-26 Sat 11:27

Author: Jack Liu

Created: 2020-09-28 Mon 19:22

Validate