1.接口
接口用来描述类应该做什么,而不指定它们具体应该如何做。一个类可以实现一个或多个接口。
1.1概念
接口不是类,而是对希望符合这个接口的类的一组需求。
//Arrays类中的sort方法承诺可以对对象数组进行排序,但要求对象所属的类必须实现Comparable接口
public interface Comparable<T>{
int compareTo(T other);
}
//在调用x.compareTo(y)时,x<y返回负数;x==y返回0;x>y返回正数。
复制代码
接口中的所有方法都自动是public
方法,在接口中声明方法时,不必提供关键字public
。
接口可以包含多个方法,可以定义常量。但绝不会有实例字段。
让类实现一个接口,通常需要完成两个步骤:
- 将类声明为实现给定的接口。
- 对接口中的所有方法提供定义。
public class Employee implements Comparable {
@Override
public int compareTo(Object otherObj) {
Employee other = (Employee) otherObj;
return Double.compare(salary,other.salary);
}
}
//为泛型Comparable接口提供一个类型参数
public class Employee implements Comparable<Employee> {
@Override
public int compareTo(Employee other) {
return Double.compare(salary,other.salary);
}
}
复制代码
1.2接口的属性
//接口不是类,具体来说,不能使用`new`运算符实例化一个接口:
x = new Comparable(...); //error
//不过,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x;
//接口变量必须引用实现了这个接口的类对象
x = new Employee(...);
//使用instanceof检查一个对象是否实现了某个特定的接口
System.out.println(x instanceof Comparable); //true
复制代码
与建立类的继承层次一样,也可以扩展接口。
//定义一个名为Moveable的接口:
public interface Moveable{
void move(double x, double y);
}
//假设一个名为Powered的接口扩展了以上Moveable接口:
public interface Powered extends Moveable{
double milesPerGallon();
double SPEED_LIMIT = 95; //常量 public static final
}
//与接口中的方法都自动被设置为public一样,接口中的字段总是public static final
复制代码
1.3接口和抽象类
abstract class Comparable{
public abstract int compareTo(Object other);
}
class Employee extends Comparable{
public int compareTo(Object other){
...
}
}
//使用抽象类可以提供CompareTo方法的实现,但是,每个类只能扩展一个类
复制代码
1.4静态和私有方法
在Java 8
中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的,只是这有违于将接口作为抽象规范的初衷。
目前为止,通常的做法都是将静态方法放在伴随类中。在标准库中,有成对出现的接口和工具类,如Collection/Collection或Path/Paths。
//由一个URI或者字符串序列构造一个文件或目录的路径
//使用Paths工具类中public static Path get(String first, String... more){}
Path path = Paths.get("jdk-12", "conf", "sec"); //class
System.out.println(path); //jdk-12\conf\sec
//Path接口中的static Path of(String first, String... more){}
Path of = Path.of("jdk-12", "conf", "sec"); //interface
System.out.println(of); //jdk-12\conf\sec
复制代码
这样一来,Paths类就不再是必要的,类似的,实现自己的接口时,没有理由再为实用工具方法提供一个伴随类。
在Java 9
中,接口总的方法可以是private
。private
方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。
1.5默认方法
可以为接口方法提供一个默认实现。必须用default
修饰符标记这样一个方法。
public interface Comparable<T>{
default int compareTo(T other){
return 0;
}
}
//不过,这种并无太大用处,每个具体实现都会覆盖这个方法。而在某些情况下,默认方法可能很有用。
//Iterator接口用于访问一个数据结构中的元素。这个接口声明了一个remove方法,如下:
public interface Iterator<E>{
boolean hasNext();
E next();
//如果要遍历访问的数据结构是只读的,就不用管remove方法,默认抛出异常
default void remove(){
throw new UnsupportedOperationException("remove");
}
}
//默认方法可以调用其他方法
public interface Collection {
int size();
default boolean isEmpty() {
return size() == 0;
}
}
复制代码
默认方法的一个重要用法是"接口演化"。以Collection
接口为例,这个接口作为Java
的一部分已经有很多年了。假设很久以前提供一个类实现了该接口:
public class Bag implements Collection
复制代码
在Java 8
中,又为这个接口增加了一个stream
方法。假设该方法不是一个默认方法,那么Bag
类将不能编译,因为它没有实现这个新方法。为接口增加一个非默认方法不能保证"源代码兼容"。不过,假设不重新编译这个类,而只是使用原先的一个包含这个类的JAR
文件,这个类还是可以正常加载。但是,如果程序在一个Bag
实例上调用Stream
方法,就会出现一个AbstractMethodError
将方法实现为一个默认方法就可以解决这两个问题。Bag
类又能正常编译了。如果没有重新编译而直接加载这个类,并在一个Bag
实例上调用Stream
方法,将调用Collection.stream
方法即默认方法。
1.6.解决默认方法冲突
如果在一个接口中讲一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?对于解决这些二义性,Java
有相应规则:
- 超类优先,如果超类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略。
- 接口冲突,如果一个接口提供一个默认方法,另一个接口提供一个同名且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
//默认方法冲突问题
interface Person {
//提供默认实现
default String getName() {
return "";
}
}
interface Named {
//不论是否提供默认实现,都会出现冲突。
//如果至少有一个接口提供了一个实现,编译器就会报错。
String getName();
// default String getName() {
// return getClass().getName() + "_" + hashCode();
// }
}
class Student implements Person, Named {
//提供一个getName方法来解决冲突,可以选择冲突方法中的一个
@Override
public String getName() {
return Person.super.getName();
}
}
//如果扩展超类同时实现一个接口,并从超类和接口继承相同的方法。在这种情况下,只会考虑超类方法,接口中的所有默认方法都会被忽略。
class Student extends Person implements Named{...}
复制代码
1.7接口与回调
回调是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。
public class TimerTest {
public static void main(String[] args) {
TimePrinter timePrinter = new TimePrinter();
//Timer(int delay, ActionListener listener)
//构造一个定时器,每经过1秒通知timePrinter监听器一次
Timer timer = new Timer(1000, timePrinter);
//启动定时器。一旦启动,定时器将调用监听器的actionPerformed
timer.start();
//停止定时器。一旦停止,定时器将不再调用监听器的actionPerformed
// timer.stop();
//static void showMessageDialog(Component parentComponent, Object message)
//显示一条提示信息和OK按钮的对话框。这个对话框位于parentComponent组件中央。为null显示屏幕中央
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent actionEvent) {
System.out.println("at the tone,the time is" + Instant.ofEpochMilli(actionEvent.getWhen()));
//Toolkit.getDefaultToolkit()获得默认的工具箱,beep()发出一声铃响
Toolkit.getDefaultToolkit().beep();
}
}
复制代码
1.8 Comparator 接口
因为String
类实现了Comparable<String>
接口,而且String
类中重写的compareTo
方法按字典顺序比较字符串。但如果希望按长度递增的顺序对字符串进行排序,而不是按字段顺序进行排序,这时并不能修改String
类。为了处理这种情况,Arrays.sort
方法有第二个版本,有一个数组和一个比较器(Comparator)作为参数,比较器是实现了Comparator
接口的类的实例。
public interface Comparator<T> {
int compare(T var1, T var2);
}
//定义一个实现Comparator<String>接口的类,并重写compare方法
class LengthComparator implements Comparator<String> {
@Override
public int compare(String s, String t1) {
return s.length() - t1.length();
}
}
String[] words = {"shinobu", "asd", "gxsb"};
Arrays.sort(words,new LengthComparator());
for(String s : words)
System.out.print(s); //asd gxsb shinobu
复制代码
2.lambda表达式
lambda表达式用来创建可以在将来某个时间点执行的代码块。通过使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或可变行为的代码
2.1 概念
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
class LengthComparator implements Comparator<String> {
@Override
public int compare(String s, String t1) {
return s.length() - t1.length();
}
}
String[] words = {"shinobu", "asd", "gxsb"};
Arrays.sort(words,new LengthComparator());
//在这里compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。
//将一个代码块传递到某个对象,这个代码快会在将来某个事件调用。
复制代码
2.2 语法
//传入代码来检查一个字符串是否比另一个字符串短
first.length() - second.length()
//需要指定first和second的类型
(String first,String second)
-> first.length()- second.length()
//如果代码要完成的计算无法放在一个表达式中,就可以想写方法一样,把这些代码放在{}中,并包含显示的return语句
(String first, String second) -> {
if (first.length() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0
}
//即使lambda表达式没有参数,仍要提供空括号,就像无参数方法一样
() -> {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
//如果可以推导出一个lambda表达式的参数类型,则可以忽略且类型。
Comparator<String> comp = (first, second) -> first.length();
//如果方法只有一个参数,而且这个参数的类型可以推导的出,还可以省略小括号
ActionListener listener = event -> System.out.println("the time is" +
Instant.ofEpochMilli(event.getWhen()));
//无需指定lambda表达式的返回类型。lambda的返回类型总是会由上下文推导得出
(String first,String second) -> first.length() - second.length();
//!!!但如果一个lambda表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。
(int x) -> {
if (x >= 0) return 1;
}
复制代码
String[] planets = {"Shinobu", "Ads", "Gxsb", "Mars", "Inori"};
System.out.println(Arrays.toString(planets));
System.out.println("Sorted in dictionary order:");
Arrays.sort(planets);
System.out.println(Arrays.toString(planets));
System.out.println("Sorted by length:");
//比较器使用lambda表达式
Arrays.sort(planets, (first, sencod) -> first.length() - sencod.length());
System.out.println(Arrays.toString(planets));
//动作监听器使用lambda表达式
Timer timer = new Timer(1000, event -> System.out.println("the time is" + new Date()));
timer.start();
复制代码
2.3 函数式接口
Java
中有很多封装代码块的接口,lambda表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口
//Arrays.sort方法第二参数需要一个Comparator实例,Compartor就是只有一个方法的接口,所以可以提供一个lambda表达式
Arrays.sort(words, (first,second)->first.length()-second.length());
//在底层,Arrays.sort方法会接收实现了Compartor<String>的某个类的对象。在这个对象上调用Compare方法会执行这个lambda表达式的体。
//这些对象和类的管理完全取决与具体实现,与传统的内联类相比,这样可能要高效的多。
//最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口.
//lambda表达式可以转换为接口,这一点让lambda表达式很有吸引力:
Timer timer = new Timer(1000, event -> {
System.out.println("at the tone,the time is "
+ Instant.ofEpochMilli(event.getWhen()));
Toolkit.getDefaultToolkit().beep();
});
//原来的做法:
class TimePrinter implements ActionListener {
@Override
public void actionPerformed(ActionEvent actionEvent) {
System.out.println("at the tone,the time is" + Instant.ofEpochMilli(actionEvent.getWhen()));
Toolkit.getDefaultToolkit().beep();
}
}
Timer timer = new Timer(1000, new TimePrinter());
复制代码
不能把lambda
表达式赋给类型为Object
的变量,Object
不是函数式接口
想要使用lambda表达式做某些处理,还要谨记表达式的用途,为它建立一个特定的函数式接口。java.util.function
包中有一个尤其有用的接口Predicate
:
public interface Predicate<T>{
boolean test(T t);
//additional default and static methods
}
复制代码
ArrayList
类有一个removeIf
方法,它的参数就是一个Predicate
。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null
值:
list.removeIf(e -> e == null);
复制代码
另一个有用的函数式接口是Supplier:
public interface Supplier<T>{
T get();
}
复制代码
供应者(Supplier)没有参数,调用时会生成一个T类型的值。供应者用于实现懒计算。某些方法只有在需要值是才调用供应者。
2.4 方法引用
有时,lambda表达式涉及一个方法。例如,
//假设希望只要出现一个定时器时间就打印这个事件对象。
Timer timer = new Timer(1000, event -> System.out.println(event));
//但是,如果把println方法传递到Timer构造器就更好了:
Timer timer = new Timer(1000, System.out::println);
//表达式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,
//覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,
//它的actionPerformed(ActionEvent e)方法要调用System.out.println(e)。
复制代码
再来看一个例子,假设想要对字符串进行排序,而不考虑字母的大小写。可以传递一下表达式:
Arrays.sort(planets, String::compareToIgnoreCase);
复制代码
2.5 构造器引用
3.内部类
内部类定义在另外一个类的内部,它们的方法可以访问包含它们它们的外部类的字段。内部类在设计具有相互写作关系的类集合时很有用。
- 内部类可以对同一个包中的其他类隐藏;
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
实际上,内部类的对象总有一个隐式对象,指向创建它的外部类对象。这个引用在内部类的定义中是不可见的,
3.1 局部内部类
当内部类只使用一次时,可以在一个方法中局部定义一个类即局部类。声明局部类时不能有访问说明符。局部类的作用域被限定在声明这个局部类的块中。局部类对外部世界完全隐藏。
3.2 匿名内部类
使用局部内部类时,通常可以再进一步。假如只想创建一个类的一个对象,不需要为类指定名字。这样一个类被称为匿名内部类。
//创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed
//在括号{}内定义
var listen = new ActionListener(){
public void actionPerformed(ActionEvent event){
...
}
};
var timer = new Timer(interval, listener);
timer.start();
//一般的,语法如下:
//new SuperType(construction parameter){
// inner class methods and data
//}
//其中SuperType可以是接口,如ActionListener,如果是这样,内部类就要实现这个接口。
//SuperType也可以是一个类,如果是这样,内部类就要扩展这个类。
复制代码
//使用lambda表达式写匿名内部类更加简洁
var timer = new Timer(interval, event -> {
...
});
timer.start();
复制代码
3.3 静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以把内部类声明为static
,这样就不会生成那个引用。