java中接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
例如:Arrays
类中的sort
方法承诺可以对对象数组进行排序,但要求满足:对象所属的类必须实现了Comparable
接口
public interface Comparable{
int compareTo(Object other);
}
也就是说,任何实现Comparable
接口的类都需要包含compareTo
方法,并且参数必须是Object
对象,返回一个int
类型值
接口中的所有方法自动属于public
,因此声明时不用提供关键字public
接口可能包含多个方法
接口绝不能包含实例域,在Java SE 8之前,也不能在接口中实现方法
要将类声明为实现某个接口,需要使用关键字implements
,例如
class Employee implements Comparable{
public int compareTo(Object otherObject){
Employee other = (Employee) otherObject;
return Double.compare(salary, other.salary);
}
}
// 可以为泛型Comparable接口提供类型参数,就不用类型转换
class Employee implements Comparable<Employee>{
public int compareTo(Employee other){
return Double.compare(salary, other.salary);
}
}
在实现接口时,必须将方法声明为public
,否则在类里默认为包可见性
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
Arrays.sort(staff);
for(Employee e : staff){
System.out.println("name = "+e.getName()+", salary = "+e.getSalary());
}
}
}
class Employee implements Comparable<Employee>{
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void raiseSalary(double byPercent){
salary += salary*byPercent/100;
}
public int compareTo(Employee other){
return Double.compare(salary, other.salary);
}
}
接口不是类,尤其不能使用new
运算符实例化一个接口
x = new Comparable(...); // error!
但是可以声明接口的变量
Comparable x; // ok
接口变量必须引用实现了接口的类对象
x = new Employee(...); // ok if provided Employee implements Comparable
类似使用instanceof
检查对象是否属于某个类,也可以使用instanceof
检查一个对象是否实现了某个特定接口
if(anObject instanceof Comparable) {...}
接口也可以继承
public interface Moveable{
void move(double x, double y);
}
// 继承
public interface Powered extends Moveable{
double milesPerCallon();
}
接口中可以包含常量
public interface Powered extends Moveable{\
double milesPerCallon();
double SPEED_LIMIY = 95;
}
接口中的域被自动设置为public static final
每个类可以实现多个接口,用逗号将实现的各个接口分割开
class Employee implements Cloneable, Comparable
接口不设计为抽象类,是为了避免多重继承。
Java SE 8中,允许在接口中增加静态方法。不过有违将接口作为抽象规范的初衷
可以为接口方法提供一个默认实现,必须用default
修饰符标记。
public interface Compare<T>{
default int compareTo(T other){
return 0;
}
}
如果在一个接口中将一个方法定义为默认方法,然后在超类或者另一个接口中定义相同的方法
考虑第一个规则,如果一个类继承自一个超类,同时实现一个接口,并且从超类和接口继承了相同的方法
class Student extends Person implements Named{...}
这种情况下,只会考虑超类方法,接口的所有默认方法都会被忽略
考虑第二个规则。如果两个接口提供了同名同参数类型的方法,一个类同时实现这两个接口,必须指定实现哪一个
class Student implements Person, Named{
@Override
public String getName() {
return Person.super.getName();
}
}
interface Person{
default String getName() {return "";}
}
interface Named{
default String getName() {return getClass().getName()+"_"+hashCode();}
}
回调(callback)是一种常见的程序设计模式。可以指定某个特定事件发生时应该采取的动作。例如在按下鼠标时应该采取什么行动。
java.swing
包中包含一个Timer
类,可以使用它在到达给定时间间隔时发出通告。
构造定时器时需要指出时间间隔,以及需要的操作。后者通过传递一个对象,并且该对象所属的类实现了java.awt.event
包的ActionListener
接口
public interface ActionListener{
void actionPerformed(ActionEvent event);
}
到达指定的时间间隔时,定时器就调用actionPerformed
方法。
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
public class Test {
public static void main(String[] args) {
ActionListener listener = new TimePrinter();
Timer t = new Timer(5000, listener);
t.start();
// 对话框
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is "+new Date()); // 打印时间信息
Toolkit.getDefaultToolkit().beep(); // 响铃
}
}
Comparator
接口6.1.1指出对一个对象数组排序,前提是对象所属的类需要实现Comparable
接口。
String
类实现了Comparable
,而String.compareTo
方法可以按照字典序排序
假如现在要按长度排序,肯定不能让String
类用两种方法实现compareTo
,况且也轮不到我们修改String
要处理这种情况,需要用Arrays.sort
的第二个版本,接受一个数组和一个比较器作为参数。比较器是实现了Comparator
接口的类的实例
import java.util.Arrays;
import java.util.Comparator;
public class Test {
public static void main(String[] args) {
String[] friend = {"Peter", "Syb", "yy"};
Comparator<String> comp = new LengthComparator(); // 需要构造一个实例,注意类型
Arrays.sort(friend, comp);
for(String fri : friend){
System.out.print(fri+" ");
}
}
}
class LengthComparator implements Comparator<String>{
public int compare(String s1, String s2){
return s1.length() - s2.length();
}
}
6.3 我们会了解到,用lambda表达式可以更容易使用Comparator
Cloneable
接口指示一个类提供了一个安全的clone
方法
为一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用,任何一个变量改变都会影响另一个。
如果希望得到一个新对象,其初始状态和原变量相同,但之后互相独立,那么就可以使用clone方法
clone
方法是Object
的一个protected
方法,我们的代码不能直接调用这个方法。
Object
实现clone
时,对对象一无所知,采用的是逐个域地进行拷贝(浅拷贝)。如果所有数据域都是数值或者其他基本类型,那么没问题;但如果包含对象引用,直接拷贝就不行了。
浅拷贝如果拷贝了不可变对象,那么这种共享是安全的,但如果是可变类型,那么就不安全。
如果默认的clone
方法不满足要求,需要在可变的子对象上调用clone
来修补默认的clone
方法,那么就应该实现Cloneable
接口,重新定义clone
方法,指定public
修饰符
clone
重定义为public
才能允许所有方法进行克隆;否则只有子类方法能够克隆Cloneable
接口与正常接口的使用没有关系。具体而言,没有指定clone
方法,这个方法是从Object
类继承的。这个接口只是作为一个标记,表示类设计者了解克隆。如果一个对象请求克隆,但没有实现这个接口,就会生出一个受查异常所有数组类型都有一个public
的clone
方法,可以直接使用
int[] a = {1,2,3,4};
int[] b = a.clone();
b[0] = 2; // 不会改变a[0]
import java.util.Date;
import java.util.GregorianCalendar;
public class Test {
public static void main(String[] args) {
try{
Employee original = new Employee("John Q. Public", 50000);
original.setHireDay(2000,1,1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original = "+original);
System.out.println("copy = "+copy);
}catch (CloneNotSupportedException e){
e.printStackTrace();
}
}
}
class Employee implements Cloneable{
private String name;
private double salary;
private Date hireDay;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = new Date();
}
public Employee clone() throws CloneNotSupportedException{
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();
return cloned;
}
public void setHireDay(int year, int month, int day){
Date newHireDay = new GregorianCalendar(year, month-1, day).getTime();
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent){
salary += salary*byPercent/100;
}
@Override
public String toString(){
return "Employee[name="+name+",salary="+salary+",hireDay="+hireDay+"]";
}
}
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次
在之前的ActionListener
的actionPerformed
方法中,如果想要反复执行这个代码,就需要构造一个类的实例,然后将这个实例提交给Timer
对象。
目前为止,在Java中传递代码段并不容易,必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码
lambda表达式是一个代码块,以及必须传入代码的变量规范
语法上,需要用(参数)->(表达式)
(String first, String second)->{
if(first.length()<second.length()) return -1;
else if(first.length() == second.length()) return 0;
return 1;
}
即使lambda表达式没有参数,也要提供空括号,就像无参数方法一样
()->{
for(int i=100;i>=0;--i)
Sout(i);
}
如果可以推导出一个lambda表达式的参数类型,就可以忽略其类型
Comparator<String> comp = (first, second)-> first.length()-second.length();
这里编译器推导出first
和second
必定是字符串,因为这个lambda表达式将赋给一个字符串比较器
无需指定lambda表达式的返回类型,其总是会由上下文推到得到
如果一个方法只有一个参数,并且类型可以推导得出,那么甚至可以省略小括号
ActionListener listener = event->System.out.println("The time is "+new Date());
如果一个lambda表达式只在某些分支返回一个值,而在其他分支不返回值,这是不合法的
以下在一个比较器和一个监听器中使用lambda表达式
import javax.swing.*;
import java.util.Arrays;
import java.util.Date;
public class Test {
public static void main(String[] args) {
String[] planets = new String[]{"Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"};
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:");
Arrays.sort(planets, (first, second)->first.length()-second.length());
System.out.println(Arrays.toString(planets));
Timer t = new Timer(1000, event-> System.out.println("The time is "+new Date()));
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");
System.exit(0);
}
}
Java有很多封装代码块的接口,例如ActionListener
或Comparator
。lambda表达式与这些接口是兼容的
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口
java.util.function
有一个尤其有用的接口Predicate
public interface Predicate<T>{
boolean test(T t);
}
ArrayList
有一个removeIf
方法,其参数就是一个Predicate
,这个接口用来传递lambda表达式。例如下面从一个数组中删除所有null
list.removeIf(e->e==null);
有时候可能有现成的方法可以完成想要传递到其他代码的某个动作
例如假设你希望,只要出现一个定时器事件,就打印这个事件对象
Timer t = new Timer(1000, event->System.out.println(event));
可以直接把println
方法传递给Timer
构造器:
Timer t = new Timer(1000, System.out::println);
System.out::println
是一个方法引用,等价于x->System.out.println(x)
假设想对字符串排序,而不考虑字母大小写,可以传递以下方法表达式
Array.sort(strings, String::compareToIgnoreCase);
::
操作符分隔方法名与对象或类名,主要有三种情况:
Object::instanceMethod
Class::staticMethod
Class::instanceMethod
前两种情况中,方法引用等价于提供方法参数的lambda表达式。例如System.out::println
等价于x->System.out.println(x)
对于第三种情况,第一个参数会成为方法的目标,例如String::compareToIgnoreCase
等价于(x,y)->x.compareToIgnoreCase(y)
构造器引用和方法引用类似,只不过方法名为new
例如Person::new
是Person
构造器的一个引用
假设有一个字符串列表,可以将其转换为一个Person
对象数组,为此要在各个字符串上调用构造器
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
可以为数组类型建立构造器引用,例如int[]::new
是一个构造器引用,有一个参数:数组的长度。等价于x->new int[x]
通常,希望在lambda表达式中访问外围方法或类中的变量,考虑以下代码
public static void repeatMessage(String text, int delay){
ActionListener = event->{
System.out.println(text);
Toolkit.getDeafultToolkit().beep();
};
new Timer(delay, listener).start();
}
// 调用
repeatMessage("Hello", 1000);
这里的text
并不是在lambda表达式中定义的。属于是自由变量,被lambda表达式捕获了
lambda表达式可以捕获外围作用域中的变量的值。在Java中,要确保捕获的值是明确定义的,只能引用值不会改变的变量(不论在外部变化还是内部变化,都非法)
lambda表达式的体和嵌套块由相同的作用域,因此适用命名冲突和遮蔽的有关规则
在一个lambda表达式中使用this
时,是指创建这个lambda表达式的方法的this
参数,例如
public class Application(){
public void init(){
ActionListener listener = event->{
System.out.println(this.toString());
...
}
...
}
}
this.toString()
会调用Application
对象的toString
方法,而不是ActionListener
的
内部类是定义在另一个类中的类