CoreJava 笔记总结-第六章 接口、lambda表达式与内部类

祁承嗣
2023-12-01

第六章 接口、lambda表达式与内部类

  • 接口: 描述类应该做什么, 不指定如何做
  • lambda表达式: 表示使用回调或者可变行为的代码

接口

接口的概念

  • 接口: 不是类, 而是对希望符合这个接口的类的一组需求

  • Arrays类中的sort方法对对象数组进行排序, 要求对象所属的内必须实现Comparable接口

public interface Comparable
{
    int compareTo(Object other);
}
//java5后
public interface Comparable<T>
{
    int compareTo(T other);
}
  • 接口中的所有方法自动为public
  • 接口中可以包含多个方法, 但是接口不会有实例字段
  • 让一个类实现一个接口:
    1. 将类声明为实现给定的接口
    2. 对接口中的所有方法提供定义
  • 将类声明为实现为某个接口, 使用关键字implements
class Employee implements Comparable
  • Java API建议equals, compareTo方法兼容
  • 例外:x = BigDecimal("1.0"); y = BigDecimal("1.00"); x.equals(y)//false x.compareTo(y) == 0
package chapter6_interface_lambda_innerClass.interfaces;

public class Employee implements Comparable<Employee>{

    private String name;
    private double salary;

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    public double getSalary() {
        return salary;
    }

    public String getName() {
        return name;
    }

    public int compareTo(Employee other) {
        return Double.compare(salary, other.salary);
    }
}

package chapter6_interface_lambda_innerClass.interfaces;

import java.util.Arrays;

public class EmployeeSortTest {

    public static void main(String[] args) {
        var staff = new Employee[3];

        staff[0] = new Employee("Oukunnan", 25598);
        staff[1] = new Employee("Ovfdunnan", 18);
        staff[2] = new Employee("dsukunnan", 98);

        Arrays.sort(staff);

        for (Employee employee : staff) {
            System.out.println(employee.getName() + "  salary=" + employee.getSalary());
        }
    }
}


接口的属性

  • 接口不是类, 不能用new运算符实例化一个接口
  • 能够声明接口变量: Comparable x ; //OK
  • 接口变量必须引用实现了这个接口的类对象 x = new Employee(...);
  • instanceof: 1. 检查一个对象是否属于某个特定的类 2. 一个对象是否实现了某个特定的接口
if (anObject instanceof Comparable){...};
  • 与建立类的继承层次类似, 可以扩展接口
public interface Moveable{...}
public interface Powered extends Moveable{..}
  • 接口不能包含实例字段但是可以包含常量
  • 接口的方法总是public, 接口的字段总是public static final, 都可以省略, 建议省略
  • 每个类只有一个超类, 却可以实现多个接口, 有点像C++的多重继承

接口与抽象类

  • 抽象类问题: 每个类只能扩展一个类
  • java可以扩展一个基类并且派生多个接口

静态和私有方法

  • 标准库中成对出现的接口和实用工具类: Collection/Collections, Path/paths
  • 允许在接口中增加静态方法, 一般做法是放在伴随类中
  • java9中接口中的方法可以是private

默认方法

  • 接口方法提供一个默认实现, default修饰符标记
public interface Comparable<T>
{
    default int CompareTo(T other) {return 0;}
}
  • 如果迭代器是只读的就不用实现remove方法

    public interface Iterator<E>
    {
        boolean hasNext();
        E next();
        default void remove(){throw new UnsupprotedOperationException("remove");}
    }
    
  • 另一个作用是接口演化, 实现源代码兼容


解决默认方法冲突

  • 超类优先
  • 同时实现的两个接口中由完全同名并且参数类型相同的方法, 要求这个类实现该方法覆盖接口的方法
  • class Student extends Person implements Named{...}只会考虑超类方法, 类优先原则

接口与回调

  • 回调: 指定某个特定事件发生时应该采取的动作
package chapter6_interface_lambda_innerClass.timer;

import java.awt.event.*;
import java.awt.*;
import java.time.*;
import javax.swing.*;

public class TimerTest {

    public static void main(String[] args) {
        var listener = new TimePrinter();
        var timer = new Timer(1000, listener);//每隔1000ms(1s)响铃并且打应输出
        timer.start();

        JOptionPane.showMessageDialog(null, "Quit Program?");//展示消息框
        System.exit(0);
    }
}

class TimePrinter implements ActionListener {
    public void actionPerformed(ActionEvent event) {
        //ActionEvent 事件参数,提供事件的相关信息              //getWhen的得到纪元以来的毫秒数, 利用函数转换成可读时间
        System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
        Toolkit.getDefaultToolkit().beep(); //调用默认工具箱响铃
    }
}


Comparator接口

  • 对于对象数组进行排序, 前提是这些对象是实现了Comparable接口类的实例
  • 如果按照长度而不是字典顺序对于字符串进行排序, 使用Arrays.sort的另一种版本, 一个数组和比较器作为参数
  • 比较器实现Comparator接口
public interface Comparator<T>
{
    int compare(T first, T second);
}

class LengthComparator implements Comparator<String>
{
    public int compare(String first, String second)
    {
        return first.length() - second.length();
    }
}

调用
Arrays.sort(words, new LengthComparator());

对象克隆

  • 拷贝: 一个包含对象引用的变量建立副本时,原变量和副本都是对同一个对象的引用, 任何一个对象的引用都会改变另一个变量
  • 克隆: 希望变量是一个新的对象, 初始状态和原变量相同, 之后会有各自不同的状态
  • 默认的克隆操作是浅拷贝: 逐个字段拷贝, 对于数值和其他基本类型克隆, 但是对于包含对象引用的子对象也会共享一些信息
  • 如果原对象和浅克隆对象共享的子对象是不可变的, 那么浅拷贝的共享安全
  • 深拷贝: 子对象可变的, 必须重新定义clone方法建立深拷贝, 克隆所有对象
  • Cloneable: 标记接口, 不包含任何方法(一般的接口确保一个类实现一组特定的方法), 作用:允许类型查询中使用instanceof
if(obj instanceof Cloneable) ...
  • 所有数组类型都有一个公共的clone方法, 不是受保护的
int[] a = {1, 2, 3};
int[] b = a.clone();
b[0] = 5;// a[0] == 1;
package chapter6_interface_lambda_innerClass.clone;

public class CloneTest {

    public static void main(String[] args) throws CloneNotSupportedException{
        var original = new Employee("Ou Kunnan", 52000);
        original.setHireDay(2001, 12, 26);
        Employee copy = original.clone();
        System.out.println(copy); // 自动调用toString方法,相当于copy.toString()
        copy.raiseSalary(10);
        copy.setHireDay(2002, 12, 26);
        System.out.println("original: " + original);
        System.out.println("copy: " + copy);
    }
}

package chapter6_interface_lambda_innerClass.clone;

import java.util.Date;
import java.util.GregorianCalendar;

public class Employee implements Cloneable{

    private double salary;
    private String name;
    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) {
        double raise = salary * byPercent / 100;
        salary += raise;
    }

    public String toString() {
        return "Employee[name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]";
    }
}


lambda表达式

  • lambda表达式就是一个代码块,以及必须传入的代码变量规范

  • 形式: 参数, ->, 一个表达式

  • (String first, String second)->{
        if(first.length()  < second.length()) return -1;
        else return 1;
    }
    
  • lambda没有参数,`()-> {…};

  • 如果可以推导出lambda表达式参数类型, 可以忽略其类型

Comparator<String> comp = (first, second) -> first.length() - second.length();
  • 只有一个参数并且类型可以推到, 可以省略小括号

  • 无需指定返回类型

package chapter6_interface_lambda_innerClass.lambda;

import java.util.*;

import javax.swing.*;
import javax.swing.Timer;

public class lambdaTest {

    public static void main(String[] args) {
        var 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));

        var timer = new Timer(1000, event -> System.out.println("The time is " + new Date()));
        timer.start();

        JOptionPane.showMessageDialog(null, "quit?");
        System.exit(0);
    }
}


函数式接口

  • 函数式接口: 对于只有一个抽象方法的接口,需要这种接口对象时, 可以提供一个lambda表达式
  • lambda表达式可以转换为接口
var timer = new Timer(1000, event->
                      {
                          System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
                          Toolkit.getDefaultToolkit().beep(); 
                      })
  • ArrayList类的removeIf方法参数是Predicate
public interface Predicate<T>
{
    boolean test(T t);
}
//删除列表所有null值
list.removeIf(e -> e == null);
  • supplier没有参数, 调用时会生成T类型的值, 用于实现计算
public interface Supplier<T>
{
    T get();
}

LocalDate hireDay = Objects.requireNonNullOrElse(day, new LocalDate(1970, 1, 1));

//通过使用供应者supplier, 延迟该计算 , 只有day==null时候才调用供应者(构造默认的LocalDate)
LocalDate hireDay = Objects.requireNonNullOrElse(day, () -> new LocalDate(1970, 1, 1));

方法引用

  • var timer = new Timer(1000, event->System.out.println(event));
    var timer = new Timer(1000, System.out::println);
    
  • System.out::println是一个方法引用, 它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法

  • 上面的例子, 会生成一个ActionListener, 他的actionPerformed(ActionEvent e)方法要调用System.out.println(e)

  • 方法引用不是对象, 为一个类型为函数式接口的变量赋值时会生成一个对象

  • 方法引用示例与等价的lambda表达式见P248

  • 当lambda表达式的体只调用一个方法而不做其他操作的时候才能把方法引用重写为方法引用


构造器引用

  • Person::new就是Person构造器的一个引用
  • int[]::new是一个构造器引用, 他有一个参数,即数组的长度, 等价于x->new int[x]

变量作用域

  • lambda表达式三个部分: 代码块, 参数, 自由变量的值
  • 可以把一个lambda表达式转换为一个包含方法的对象, 自由变量的值会复制到这个对象的实例变量中
  • lambda表达式是闭包的
  • lambda表达式可以捕获外围作用域中变量的值, 确保值是明确定义的(事实最终变量, 初始化后不会改变)

处理lambda表达式

  • lambda表达式重点是延迟执行

  • Runnable作为无参数或返回值的动作运行, action.run()会调用lambda表达式主体

    package test;
    
    import java.util.function.IntConsumer;
    
    public class lambda {
        public static void main(String[] args)
        {
            repeat(10, ()->System.out.println("hello, world"));
            repeat(10, (i)->System.out.println("Countdown:" + (9-i)));
        }
    
        public static void repeat(int n, Runnable action)
        {
            for(int i = 0; i < n; i++) action.run();
        }
    
        public static void repeat(int n, IntConsumer action) {
            for(int i = 0; i < n; i++)action.accept(i);
        }
    }
    

再谈Comparator

  • P255

内部类

  • 定义在另一个类中的类
  • 内部类可以对同一个包中的其他类隐藏, 内部类方法可以访问定义这个类的作用域中的数据,包括原本的私有数据

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

  • 一个内部类方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段
  • 内部类对象总有一个隐式引用指向创建它的外部类对象
  • 这个引用在构造器中设置, 编译器会修改所有内部类的构造器,添加一个对应外围类引用的参数
以下这个例子生成的无参数构造器如下
public TimePrinter(TalkingClock clock){outer = clock;}
package chapter6_interface_lambda_innerClass.innerClass;
import java.awt.*;
import java.awt.event.*;
import java.time.*;

import javax.swing.*;

public class innerClassTest {
    public static void main(String[] args) {
        var clock = new TalkingClock(1000, true);
        clock.start();

        JOptionPane.showMessageDialog(null, "Quit?");
        System.exit(0);
    }
}

class TalkingClock{
    private int interval;
    private boolean beep;

    public TalkingClock(int interval, boolean beep) {
        this.interval = interval;
        this.beep = beep;
    }

    public void start() {
        var listener = new TimerPrinter();
        var timer = new Timer(interval, listener);
        timer.start();
    }

    public class TimerPrinter implements ActionListener {
        public void actionPerformed(ActionEvent event) {
            System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
            if (beep) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }

}

局部内部类

  • TimePrinter名字只出现了一次,在start方法中创建这个类型对象时使用了一次.可以在一个方法中局部定义这个类

    public void start() {
        class TimerPrinter implements ActionListener {
            public void actionPerformed(ActionEvent event) {
                System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
                if (beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        }
        
        var listener = new TimerPrinter();
        var timer = new Timer(interval, listener);
        timer.start();
    }
    
  • 局部内部类声明时候不能有访问修饰符public, private

  • 优势: 对外部完全隐藏,除了start代码


由外部方法访问变量

  • 局部内部类不仅能够访问外部类字段,还可以访问局部变量(事实最终变量)

  • public void start(int interval, boolean beep) {
        class TimerPrinter implements ActionListener {
            public void actionPerformed(ActionEvent event) {
                System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
                if (beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        }
        
        var listener = new TimerPrinter();
        var timer = new Timer(interval, listener);
        timer.start();
    }
    

匿名内部类

  • 匿名内部类不需要为类指定名字

  • 以下代码: 创建了一个类的新对象,这个类实现了ActionListener接口, 需要实现的方法{}中定义

    public void start(int interval, boolean beep) {
        var listener = new ActionListener();
        {
            public void actionPerformed(ActionEvent event) {
                System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
                if (beep) {
                    Toolkit.getDefaultToolkit().beep();
                }
            }
        };
        var timer = new Timer(interval, listener);
        timer.start();
    }
    
    //用lambda表达式
    public void start(int interval boolean beep)
    {
        var timer = new Timer(interval, event->{
            System.out.println("At this tone, the time is " + Instant.ofEpochMilli(event.getWhen()));
            if (beep) Toolkit.getDefaultToolkit().beep();
        })
    }
    
  • 语法如下

new SuperType(construction parameters)
{
    innner class methods and data
}

SuperType可以是接口,内部类就要实现这个接口;如果是一个类,内部类就要扩展这个类

  • 构造器名字必须和类名相同,匿名内部类没有类名所以没有构造器
  • 构造参数要传递给超类构造器
  • 注意: 构造一个类的新对象和构造一个扩展了那个类的匿名内部类的对象之间的差别
vae queen = new Person("Marry");
var count = new Person("bjcs"){...};
  • 匿名内部类不能有构造器但是可以提供一个对象的

  • 双括号初始化:

  • var f  = new ArrayList<String>();
    f.add("Harry");
    f.add("Alice");
    invite(f);
    --->
    invite(new ArrayList<String>)(){{add("Harry"); add("Alice");}}  
    //外层括号建立了一个匿名子类,内层括号是一个初始化块
    
  • 得到匿名内部类的外部类类名不能直接getClass,这个方法带调用this.getClass(), 静态方法没有隐式参数

new Object(){}.getClass().getEnclosingClass()

new Object()建立Object的匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类


静态内部类

  • 只要内部类不需要访问外围类对象,就应该使用静态内部类
  • 接口中声明的内部类自动为public, static
package chapter6_interface_lambda_innerClass.staticInnerClass;

public class StaticInnerClassTest
{
    public static void main(String[] args)
    {
        var values = new double[20];
        for (int i = 0; i < values.length; i++)
            values[i] = 100 * Math.random();
        ArrayAlg.Pair p = ArrayAlg.minmax(values);
        System.out.println("min = " + p.getFirst());
        System.out.println("max = " + p.getSecond());
    }
}

class ArrayAlg{

    public static class Pair {
        /* 一个静态的内部类 */
        private double first;
        private double second;

        /**
         * Constructs a pair from two floating-point numbers
         *
         * @param f the first number
         * @param s the second number
         */
        public Pair(double f, double s) {
            first = f;
            second = s;
        }

        /**
         * Returns the first number of the pair
         *
         * @return the first number
         */
        public double getFirst() {
            return first;
        }

        /**
         * Returns the second number of the pair
         *
         * @return the second number
         */
        public double getSecond() {
            return second;
        }
    }
        /**
         * Computes both the minimum and the maximum of an array
         * @param values an array of floating-point numbers
         * @return a pair whose first element is the minimum and whose second element
         * is the maximum
         */
        public static Pair minmax(double[] values)
        {
            double min = Double.POSITIVE_INFINITY;
            double max = Double.NEGATIVE_INFINITY;

            for (double v : values) {
                if (min > v) {
                    min = v;
                }
                if (max < v) {
                    max = v;
                }
            }
            return new Pair(min, max);
        }
}

代理

何时使用代理

  • 代理类在运行时闯将全新的类,这样代理类可以实现你指定的接口
  • 代理类包含的方法: 指定接口所需要的全部方法, Object类中的全部方法(equals, toStirng等)

创建代理对象

  • 需要使用Proxy类的newProxyInstance方法, 有三个参数
    • 一个类加载器(这里指定系统类加载器)
    • 一个Class对象数组,每个元素对应需要实现的各个接口
    • 一个调用处理器
  • 代理作用: 将方法调用路由到远程服务器;在运行中的程序将用户界面事件与动作关联起来;为了调试,跟踪方法使用
package chapter6_interface_lambda_innerClass.proxy;

import java.lang.reflect.*;
import java.util.*;

public class ProxyTest
{
    public static void main(String[] args) {
        var elements = new Object[1000];

        for (int i = 0; i < elements.length; i++) {
            Integer value = i + 1;
            var handler = new TraceHandler(value);
            Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                    new Class[]{Comparable.class}, handler);
            elements[i] = proxy;
        }

        Integer key = new Random().nextInt(elements.length) + 1;

        int result = Arrays.binarySearch(elements, key);

        if(result >= 0) System.out.println(elements[result]);
    }
}

class TraceHandler implements InvocationHandler {//打应输出方法名和参数,并且调用该方法
    private Object target;

    public TraceHandler(Object t) {
        target = t;
    }

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
        System.out.print(target);
        System.out.print("." + m.getName() + "(");

        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                System.out.print(args[i]);
                if(i < args.length - 1) System.out.print(", ");
            }
        }
        System.out.println(")");

        return m.invoke(target, args);
    }
}

代理的特性

  • 代理是在运行过程中创建的,一旦创建就变成了常规类
  • 代理类都是扩展Proxy, 一个代理类只有一个实例字段即调用处理器,在超类Proxy中定义
  • 所有的代理类都要覆盖toString, hasCode, equals方法, 这些方法只是在调用处理器上调用invoke.

 类似资料: