【Java核心技术卷I笔记】 第六章 接口、lambda表达式与内部类

文章目录

  • 6.1 接口
    • 6.1.1 接口概念
    • 6.1.2 接口的特性
    • 6.1.3 接口与抽象类
    • 6.1.4 静态方法
    • 6.1.5 默认方法
    • 6.1.6 解决默认方法冲突
  • 6.2 接口示例
    • 6.2.1 接口与回调
    • 6.2.2 `Comparator`接口
    • 6.2.3 对象克隆
  • 6.3 lambda表达式
    • 6.3.1 为什么引入lambda表达式
    • 6.3.2 lambda表达式的语法
    • 6.3.3 函数式接口
    • 6.3.4 方法引用
    • 6.3.5 构造器引用
    • 6.3.6 变量作用域
    • 6.3.7 处理lambda表达式(TODO
  • 6.4 内部类
    • 6.4.1 使用内部类访问对象状态

6.1 接口

6.1.1 接口概念

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);
    }
}

6.1.2 接口的特性

接口不是类,尤其不能使用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

6.1.3 接口与抽象类

接口不设计为抽象类,是为了避免多重继承。

6.1.4 静态方法

Java SE 8中,允许在接口中增加静态方法。不过有违将接口作为抽象规范的初衷

6.1.5 默认方法

可以为接口方法提供一个默认实现,必须用default修饰符标记。

public interface Compare<T>{
	default int compareTo(T other){
		return 0;
	}
}

6.1.6 解决默认方法冲突

如果在一个接口中将一个方法定义为默认方法,然后在超类或者另一个接口中定义相同的方法

  1. 超类优先。如果超类提供一个具体方法,同名而且参数类型系统的默认方法会被忽略
  2. 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供一个同名并且参数类型相同的方法,那么必须覆盖这个方法来解决冲突

考虑第一个规则,如果一个类继承自一个超类,同时实现一个接口,并且从超类和接口继承了相同的方法

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();}
}

6.2 接口示例

6.2.1 接口与回调

回调(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(); // 响铃
    }
}

6.2.2 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

6.2.3 对象克隆

Cloneable接口指示一个类提供了一个安全的clone方法
为一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用,任何一个变量改变都会影响另一个。
如果希望得到一个新对象,其初始状态和原变量相同,但之后互相独立,那么就可以使用clone方法

clone方法是Object的一个protected方法,我们的代码不能直接调用这个方法。
Object实现clone时,对对象一无所知,采用的是逐个域地进行拷贝(浅拷贝)。如果所有数据域都是数值或者其他基本类型,那么没问题;但如果包含对象引用,直接拷贝就不行了。
浅拷贝如果拷贝了不可变对象,那么这种共享是安全的,但如果是可变类型,那么就不安全。

如果默认的clone方法不满足要求,需要在可变的子对象上调用clone来修补默认的clone方法,那么就应该实现Cloneable接口,重新定义clone方法,指定public修饰符

  1. 必须将clone重定义为public才能允许所有方法进行克隆;否则只有子类方法能够克隆
  2. 这里Cloneable接口与正常接口的使用没有关系。具体而言,没有指定clone方法,这个方法是从Object类继承的。这个接口只是作为一个标记,表示类设计者了解克隆。如果一个对象请求克隆,但没有实现这个接口,就会生出一个受查异常

所有数组类型都有一个publicclone方法,可以直接使用

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+"]";
    }
}

6.3 lambda表达式

6.3.1 为什么引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次
在之前的ActionListeneractionPerformed方法中,如果想要反复执行这个代码,就需要构造一个类的实例,然后将这个实例提交给Timer对象。

目前为止,在Java中传递代码段并不容易,必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码

6.3.2 lambda表达式的语法

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();

这里编译器推导出firstsecond必定是字符串,因为这个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);
    }
}

6.3.3 函数式接口

Java有很多封装代码块的接口,例如ActionListenerComparator。lambda表达式与这些接口是兼容的
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口

java.util.function有一个尤其有用的接口Predicate

public interface Predicate<T>{
	boolean test(T t);
}

ArrayList有一个removeIf方法,其参数就是一个Predicate,这个接口用来传递lambda表达式。例如下面从一个数组中删除所有null

list.removeIf(e->e==null);

6.3.4 方法引用

有时候可能有现成的方法可以完成想要传递到其他代码的某个动作
例如假设你希望,只要出现一个定时器事件,就打印这个事件对象

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);

::操作符分隔方法名与对象或类名,主要有三种情况:

  1. Object::instanceMethod
  2. Class::staticMethod
  3. Class::instanceMethod

前两种情况中,方法引用等价于提供方法参数的lambda表达式。例如System.out::println等价于x->System.out.println(x)
对于第三种情况,第一个参数会成为方法的目标,例如String::compareToIgnoreCase等价于(x,y)->x.compareToIgnoreCase(y)

6.3.5 构造器引用

构造器引用和方法引用类似,只不过方法名为new
例如Person::newPerson构造器的一个引用

假设有一个字符串列表,可以将其转换为一个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]

6.3.6 变量作用域

通常,希望在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

6.3.7 处理lambda表达式(TODO

6.4 内部类

内部类是定义在另一个类中的类

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据
  • 内部类可以对同一个包中的其他类隐藏起来
  • 想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷

6.4.1 使用内部类访问对象状态

你可能感兴趣的:(Java核心技术卷I,java,开发语言,算法)