接口就像一个只有抽象方法的类,一个接口显然不能被实例化,接口会被其他的类通过类似继承的方法实现。当一个类实现接口的时候,这个类要重写接口中所有的方法。
接口的一般形式如下:
public interface InterfaceName
{
(Method headers . . .)
}
接口的形式与类十分相似,它只是将“class”变成了“Interface”。下面看一个例子:
public interface Displayable
{
void display();
}
这个类将实现上面的接口:
public class Person implements Displayable
{
private String name;
// Constructor
public Person(String n)
{
name = n;
}
// display method
public void display()
{
System.out.println("My name is " + name);
}
}
如果你希望实现一个接口,那么就在类的头中加上implements关键字,在implements关键字后加上这个类实现的接口名即可。
注意,接口中的方法都是隐式public的(也必须是public的),因此接口中的方法并不需要访问标识符(写public也行,但是没有必要)。
一个接口就像一个合同,规定了类里需要有哪些方法。但是其实接口也只是指定了方法的返回类型,方法的名字,以及方法的参数,方法具体干什么并没有指定,你可以写一个和方法体和方法名完全不相干的方法,但是这是不符合编程习惯的。
2、接口中的字段接口中也可以有字段的声明,但是这些字段一旦被声明,就将自动地被设置为final和static的,所有你必须再声明的同时赋值,看下面的例子:
public interface Doable
{
int FIELD1 = 1;
int FIELD2 = 2;
(Method headers . . .)
}
FIELD1和FIELD2都是final static int。所有实现这个接口的类都可以访问这两个字段。
3、实现多个接口你可能会疑惑:为什么抽象类和接口那么相似,还要发展出两个概念呢?那是因为Java中一个类只能有一个直接的超类,但是却可以同时实现多个接口。当一个类实现多个接口的时候,它必须重写这些接口中所有的方法。下面是一个类实现多个接口的例子:
public class MyClass implements Interface1,
Interface2,
Interface3
4、接口与UML
在UML图中,接口是像类一样绘制,但是接口的名字和方法名字要写成斜体,并且<
自从java8开始,接口也可以有默认方法,也就是有方法体的方法。看下面的例子:
public interface Displayable
{
default void display()
{
System.out.println("This is the default display method.");
}
}
当一个类实现有默认方法的接口的时候,类可以重写这个方法,也可以不重写这个方法,这并不是必须的。比如Person类就没有重写display方法。
public class Person implements Displayable
{
private String name;
// Constructor
public Person(String n)
{
name = n;
}
}
默认方法的好处就是如果你希望在接口里新增加一个方法,你不必重写所有实现接口的类。
6、多态和接口java除了可以创建某种类的类型的变量之外,还可以创建某个接口类型的变量。一个接口类型的变量可以引用任何实现这个接口的类的对象。下面是例子:
public interface RetailItem
{
public double getRetailPrice();
}
这个接口有一个方法,下面两个类将实现接口。
public class CompactDisc implements RetailItem
{
private String title; // The CD's title
private String artist; // The CD's artist
private double retailPrice; // The CD's retail price
public CompactDisc(String cdTitle, String cdArtist,
double cdPrice)
{
title = cdTitle;
artist = cdArtist;
retailPrice = cdPrice;
}
//... ...
public class DvdMovie implements RetailItem
{
private String title; // The DVD's title
private int runningTime; // Running time in minutes
private double retailPrice; // The DVD's retail price
public DvdMovie(String dvdTitle, int runTime,
double dvdPrice)
{
title = dvdTitle;
runningTime = runTime;
retailPrice = dvdPrice;
}
//... ... ... ...
因为这两个类都实现了接口,所以以下语句是合法的:
RetailItem item1 = new CompactDisc("Songs From the Heart", "Billy Nelson",
18.95);
RetailItem item2 = new DvdMovie("Planet X", 102, 22.95);
当CompactDisc和DvdMovie类实现接口的时候,一种叫做“接口继承”的继承方式出现了。它也有“is a relationship”,即“a CompactDisc object is a RetailItem, and likewise, a DvdMovie object is a RetailItem”,所以RetailItem变量才可以引用CompactDisc和DvdMovie。
在使用接口的时候当然还有一些限制,比如之前提到的,你永远不能实例化一个接口。(注意将实现接口的类的对象赋值给接口类型的变量并不是接口的实例化,而是那个类的实例化。实例化指的是创建了一个新的对象而非一个新的变量。)在你使用接口变量引用对象的时候,你只能调用接口里声明的方法,而不能使用在类里而不在接口里的方法。这和类和类之间继承时的动态绑定时一样的。
// Reference a CompactDisc object with a RetailItem variable.
RetailItem item = new CompactDisc("Greatest Hits",
"Joe Looney Band",
18.95);
// Call the getRetailPrice method . . .
System.out.println(item.getRetailPrice()); // OK, this works.
// Attempt to call the getTitle method . . .
System.out.println(item.getTitle()); // ERROR! Will not compile!
当然,一种比较古怪的写法是将接口变量强制转换为类变量,刚刚的例子中的代码可以这样写:
System.out.println(((CompactDisc)item).getTitle());二、匿名内部类
有时候你需要的类十分简单,而且只需要使用一次,这时候就可以使用匿名内部类。匿名内部类没有名字,所以只能new一次。新建匿名内部类的语法如下:
new ClassOrInterfaceName() {
(Fields and methods of the anonymous class…)
}
匿名内部类可以写在别的类里,也可以写在接口当中。但是不论写在哪里,它都需要重写方法(如果这个匿名内部类继承父类,那就要重写父类的方法,如果这个匿名内部类写在接口中或者在别的类中new了接口(看上去是实例化接口,实际上是匿名内部类,所以匿名内部类和接口放在一起说),就要实现接口中的方法)。
1、匿名内部类必须实现接口或者继承父类。
2、如果匿名内部类继承自某父类,当这个匿名内部类对象被创建时,这个父类的构造器将会先被调用。
3、匿名内部类必须重写父类的所有抽象方法,或者实现接口中的方法。
下面看例子:
这是一个接口:
interface IntCalculator
{
int calculate(int number);
}
这是匿名内部类:
IntCalculator square = new IntCalculator() {
public int calculate(int number)
{
return number * number;
}};
首先它声明了一个叫做square的intcalculator类型的变量(这是一个接口类型的变量,意味着它可以指向任何实现这个接口的类的实例)。new IntCalculator()表明创建了一个匿名内部类实现接口。
这里看起来和实例化接口一模一样,但是我们应该注意:正常的实例化语句在new这一行的结尾是分号,而匿名内部类new这一行的结尾却是括号,所以这并不是应该正常的实例化语句,应该将new IntCalculator(){}看成一个整体。当new IntCalculator(){}一起出现的时候,这里new IntCalculator()的语义就变为:new一个没有名字的新类,这个类将实现IntCalculator接口(IntCalculator()是让编译器明白要实现的接口名字叫IntCalculator,而不是创建IntCalculator接口的对象)。
三、函数式接口和Lambda表达式 1、函数式接口和Lambda表达式java8中包含了两个新的特性:函数式接口和Lambda表达式。一个函数式接口就是只有一个抽象方法的接口,比如我们之前看到的IntCalculator接口。
因为这是一个函数式接口,所以我们不需要特定地创建一个类实现它,甚至不需要创建一个匿名内部类,我们只需要运用Lambda表达式即可,我们可以使用lambda表达式来创建一个实现接口的对象,并覆盖其抽象方法。
你可以把Lambda表达式想象成一个匿名的方法。lambda表达式可以接受参数并返回值。下面是一个简单的lambda表达式的一般格式,它接受一个参数,并返回一个值:
x −> x * x
出现在−>操作符左侧的x是一个参数变量的名称,而出现在->操作符右侧的表达式x*x是返回的值。这个lambda表达式的工作原理就像将x传入方法,返回x*x的值。
我们可以使用这个lambda表达式来创建一个实现IntCalculator接口的对象。以下是一个示例:
IntCalculator square = x −> x * x;
在这个简单的语句背后,深层次逻辑式这样的:
1、因为我们用IntCalculator来表示square的类型,所以这个对象(?)将自动实现IntCalculato接口。
2、因为接口中只有一个抽象方法(函数式接口),所以这个Lambda表达式将被用来实现接口中唯一的抽象方法。
3、x和x*x不用设置参数类型,因为这个Lambda表达式的“匿名方法”实际上已经被指定好了参数的类型和返回的类型(就是接口中的那一个方法)。
Lambda表达式提供了一个更简单的实现匿名内部类的途径,更简明,简洁。
2、Lambda表达式的用法如果函数接口的抽象方法为void(不返回值),则与该接口一起使用的任何lambda表达式也应为空:
x −> System.out.println(x);
将x传入,打印x。
(a, b) −> a + b;
传入a,b,返回a+b。
() −> System.out.println();
没有参数,打印一个空格。
你也可以显式地表明传入的参数类型:
(int x) −> x * x;
(int a, int b) −> a + b;
当方法体不止一行的时候:
(int x) −> {
int a = x * 2;
return a;
};
注意返回语句和正常的方法是一样的。
3、使用lambda表达式时访问变量lambda表达式可以访问声明在同一个花括号里的变量,只要这个变量是final的或者不是final的但值从来没有改变过的。看下面的例子:
import java.util.Scanner;
public class LambdaDemo2
{
public static void main(String[] args)
{
final int factor = 10;
int num;
// Create a Scanner object for keyboard input.
Scanner keyboard = new Scanner(System.in);
// Create an object that implements IntCalculator.
IntCalculator multiplier = x −> x * factor;
// Get a number from the user.
System.out.print("Enter an integer number: ");
num = keyboard.nextInt();
// Display the number multiplied by 10.
System.out.println("Multiplied by 10, that number is " +
multiplier.calculate(num));
}
}
(把x −> x * factor赋值给multiplier的时候,编译器就会找:这个语句应该匹配哪个接口或者父类,因为multiplier是一个IntCalculator 类型的变量,所以编译器就找到了IntCalculator接口,又因为这个接口是函数式接口,所以编译器就按照里面唯一的方法使用这个lambda表达式)
当我们移除关键字final的时候,依然可以通过编译,因为factor从来没有被改变过。



