继承和多态
继承机制的使用可以复用一些定义好的类,减少重复代码的编写。多态机制可以动态调整对象的调用,降低对象之间的依存关系。
封装
封装将类的某个信息隐藏在类内部,不允许外部程序直接访问,只能通过该类提供的方法来实现对隐藏信息的操作和访问。
例如:一台计算机内部极其复杂,有主板、CPU、硬盘和内存,而一般用户不需要了解它的内部细节,不需要知道主板的型号、CPU 主频、硬盘和内存的大小,于是计算机制造商将用机箱把计算机封装起来,对外提供了一些接口,如鼠标、键盘和显示器等,方便用户使用计算机。
封装的特点:
- 只能通过规定的方法访问数据;
- 隐藏类的实例细节,方便修改和实现。
实现封装的具体步骤如下:
- 修改属性的可见性来限制对属性的访问,一般设为
private
; - 为每个属性创建一对赋值(
setter
)方法和取值(getter
)方法,一般设为public
,用于属性的读写; - 在赋值和取值方法中,加入属性控制语句(对属性值的合法性进行判断)。
下面我们来看一个完整的例子,要求编写表示图书的 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 中 super
和 this
关键字的异同,可简单总结为以下几条:
(1)子类和父类中变量或方法名称相同时,用 super
关键字来访问。可以理解为 super
是指向自己父类对象的一个指针,在子类中调用父类的构造方法。
(2) this
是自身的一个对象,代表对象本身,可以理解为 this
是指向对象本身的一个指针,在同一个类中调用其它方法。
(3) this
和 super
不能同时出现在一个构造方法里面,因为 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.name
和animal.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
的方法不能被重写,但是能够再再次声明; - 构造方法不能被重写;
- 子类和父类在同一个包中时,子类可以重写父类的所有方法,除了声明为
private
和final
的方法; - 子类和父类不在同一个包中时,子类只能重写父类的声明为
public
和protected
的非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
运算符只能用作对象的判断。
当 obj
为 null
时,直接返回 false
,因为 null
没有引用任何对象,所以 obj
的类型必须是引用类型或空类型,否则会编译错误。
另外,当 class
为 null
时,会发生编译错误,如下:
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
不为 null
和 obj
不为 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 个特征如下:
- 抽象方法没有方法体;
- 抽象方法必须存在于抽象类中;
- 子类重写父类时,必须重写父类的所有的抽象方法。
*注:在使用 abstract
关键字修饰抽象方法时不能使用 private
修饰,因为抽象方法必须被子类重写。
抽象类的定义和使用规则如下:
- 抽象类和抽象方法都要使用
abstract
关键字声明; - 如果一个方法被声明为抽象的,那么这个类也必须声明为抽象的,而一个抽象类中,也可以包含具体方法;
- 抽象类不能被实例化,也就是不能使用
new
关键字创建对象。
来看一个具体的例子吧。
不同几何图形的面积计算公式是不同的,但是它们具有的特性是相同的,都具有长和宽这两个属性,也都具有面积计算的方法。那么可以定义一个抽象类,在该抽象类中含有两个属性( width
和 height
)和一个抽象方法 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
表示变量名称,一般是static
和final
型的;return_type
表示方法返回值的类型;parameter_list
表示参数列表,在接口中的方法是没有方法体的。
*注:一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
接口对于其声明、变量和方法都做了许多限制,这些限制作为 接口的特征 如下:
- 具有
public
访问控制符的接口,允许任何类使用,否则其访问权限局限 于所属的包; - 方法的声明不需要其他修饰符,在接口中声明的方法,将隐式地声明为公有的(
public
)和抽象的(abstract
); - 在 Java 接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为
public、static
和final
,即常量,所以接口中定义的变量 必须初始化 ; - 接口没有构造方法,不能被实例化。
*注:一个接口不能够实现另一个接口,只可以继承接口,子接口可以对父接口的方法和常量进行重写。
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: }
方法 calculate
中 opr
参数是运算符,返回值是实现 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…