Core Java 读后感 - 第六章 接口、lambda表达式与内部类

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


6.1 接口


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

  • 接口所有方法 都自动是 public方法。因此,在接口中声明方法时,不必提供关键字 public

  • 接口 可以定义 常量, 但是 绝不会有 实例字段。👿 👿 👿

    • 可以将接口其看成没有实例字段的抽象类。但两者还有着一定的区别,如果将接口设计为抽象类,会存在一个严重的问题,即:每个类只能扩展一个类,假设某个类已经扩展了别的类,他就不能再扩展第二个类了。(Java不支持多重继承
  • 一个具体例子:Arrays类 中 的 sort() 方法承诺可以对对象数组进行排序,但必须满足的条件是:对象所属类必须实现 Comparable接口

    public interface Comparable
    {
    	int compareTo(Object other);
    }
    // java 5 开始
    public interface Comparable<T>
    {
    	int compareTo(T other);
    }
    
    • 假设希望使用 Arrays类sort方法Employee对象数组 进行排序, Employee类 就必须实现 Comparable接口

      // 将类声明为实现给定接口
      class Employee implements Comparable
      {
          ......
          // 对接口中的所有方法提供定义
          public int compareTo(Object otherObject)
          {
          	Employee other = (Employee) otherObject;
              return Double.compare(salary, other.salary);
          }
      }
      // java 5 开始
      class Employee implements Comparable<Employee>
      {
          ......
          public int compareTo(Employee other)
          {
              return Double.compare(salary, other.salary);
          }
      }
      
    • x.compareTo(y) : x 小于 y,返回负数;x 等于 y,返回0; x 大于 y,返回正数。

    • Comparable接口 的文档建议:compareTo方法 应当与 equals方法 兼容,也即是说当 x.equals(y) 时, x.compareTo(y) 就应该等于 0。

    • 如果比较的两个对象 一个是父类 eg:Employee,一个是子类 eg: Manager,x.compareTo(y) 不会异常, y.compareTo(x) 会出现异常 ClassCastException。(与 equals方法 相似)两种解决方法👿 👿 👿

      • 不同子类中比较的含义不同时,就应该将属于不同类的对象之间的比较视为违法,在开始 compareTo方法 时,进行如下检查: if(getClass() != other.getClass()) throw new ClassCastException()

      • 如果存在一个比较子类对象的通用方法,那么可以在超类中提供一个 compareTo方法 ,并将这个方法声明为 final

  • 接口不是 类,不能用 new 运算符实例化一个接口,虽然不能构造接口对象 ,但是可以声明接口的引用变量。每个类只能有一个超类,但是可以实现多个接口

  • 如同使用 instanceof 检查一个对象是否属于某个特定类一样,也可以使用 instanceof 检查一个对象是否实现了某个特定的接口: if(anObject instanceof Comparable){...}

  • 接口中的方法 都会被自动设置为 public ,接口中的字段总是 public static final👿 👿 👿

  • 以前的做法通常都是将 静态方法 放在 伴随类 中,在标准库中,你会看到成对出现的 接口实用工具类,eg:Collection/Collections 或者 Path/Paths 。但是,在 java8 开始,允许在接口中 增加 静态方法,实现你的接口时,没有必要为实用工具方法另外提供 伴随类

  • java9 中接口的方法可以是 privateprivate方法 可以是 静态方法实例方法。他的用法很有限,只能作为接口中其他方法的辅助方法

  • 可以为 接口 提供一个 默认实现(默认方法),必须default 修饰符标记这样一个方法。默认方法的一个重要用法是 “接口演化

    • 冲突问题:在一个接口将一个方法定义为默认方法,又在 超类 或 另外一个接口 中定义同样的方法,会发生 “二义性” 问题,解决规则如下:

      • 超类优先: 如果超类提供一个具体方法,同名而且有相同类型参数的默认方法会被忽略
      • 接口冲突: 如果一个接口提供一个默认方法,因一个接口提供了一个同名且参数类型相同(不论是否为默认方法)的方法,必须覆盖这个方法来解决冲突
      • 如果两个接口都没有给共享方法提供默认实现,这里就不存在冲突
  • 回调 : 是一种常见的程序设计模式。在这种模式中,可以指定某个特定事件发生时应该采取的动作。

  • Sting.compar()方法 可以按照字典顺序比较字符串,可是当我们需要按照长度去比较字符串时,就需要实现 Arrays.sort() 的第二个版本:

    public interface Comparator<T>{
        int comapre(T first, T second);
    }
    

    ​ 按照长度实现可以定义下面这个类

    class LengthComparator implements Comparator<String>
    {
    	public int compare(String first, String second)
    	{
    		return first.length() - second.length();
    	}
    }
    // 具体完成比较时,需要建立一个实例
    var comp = new LengthComparator()
    // 这个比较方法要在比较器对象上调用,而不是在字符串本身上调用
    if(comp.compare(words[i], words[j]) > 0) ...
    

    ​ 对数组排序时,需要为 Arrays.sort()传入一个 LengthComparator对象

    String friends = {"Peter", "Paul","Mary"};
    Arrays.sort(friends, new LengthComparator());
    
  • Cloneable 接口:

    • object类clone,他对这个对象一无所知, 所以这能逐个字段进行拷贝。如果是基本类型就没有问题,但是如果这个对象字段中包含对象引用(子对象是不可变不影响),这样一来拷贝下来仍然会出现局部共享。所以出现下面这两个概念:

      • “浅拷贝”: (Object类中clone方法的默认操作):并没有clone 对象中字段引用的其他对象

      • “深拷贝”: 同时克隆所有子对象

      • 对于每一个类需要确定:

        • 默认的 Object类 中的clone方法是否满足需求;

        • 是否可以在可变的子对象上调用 clone方法 来弥补默认的clone 方法,如果需要:

          • 实现 Cloneable 接口
          • 重新定义 clone 方法,并指定 public 访问修饰符
  • Object类 中的 clone方法 声明为 protected, 所以不能在自己写的代码里调用 anObject.clone, 因为你的类不是 该对象的子类,且你可能和定义clone方法的类不在一个包下,所以在重新定义clone方法时,要指定为 public,以便所有方法都可以克隆对象

  • Cloneable接口 没有 clone方法 ,这个方法是从 Object类 继承的。它是Java中少数的 标记接口标记接口 不含任何方法,就是在类检查中允许使用 instanceof

  • 即使默认 clone方法 实现能够满足要求,但还是需要实现 Cloneable接口 , 将 clone 重新定义为 public, 在调用super.clone()

    class Employee implements Cloneable
    {
        // public access, change return type
        public Employee clone() throws CloneNotSupportedException
        {
            return (Employee)super.clone();
        }
        ...
    }
    
  • “深拷贝” 需要克隆对象中可变的实例字段

    class Employee implements Cloneable
    {
        ...
        // 在一个对象上调用 clone 方法,若没有实现Cloneable接口, 会抛出 
        //CloneNotSupportedException异常
        public Employee clone() throws CloneNotSupportedException
        {
            // call Object.clone()
            Employee cloned = (Employee) super.clone();
            
            // clone mutable fields
            cloned.hireDay = (Date) hireDay.clone();
            
            return cloned;
        }
    }
    
  • 所有的数组类型都有一个 **公共的 ** clone方法,而不是 受保护的。可以用这个方法 建立一个新数组,包含原数组的所有副本。

    int[] luckNumbers = {2, 3, 5, 7, 11, 13};
    int[] cloned = luckNumbers.clone();
    cloned[5] = 12 // doesn't change luckNumbers[5]
    

6.2 lambda 表达式


  • lambda表达式 :是一个 可传递的代码块

    • 例子

      class LengthComparator implements Comparator<String>
      {
      	public int compare(String first, String second)
      	{
      		return first.length() - second.length();
      	}
      }
      ...
      Arrays.sort(friends, new LengthComparator());
      
    • 这个例子的特点:将一个 代码块 传递给某个对象。这个代码块会在将来某个时间调用

    • 再出现这个与法之前,Java传递一个代码块并不容易,你不能直接传递代码块,因为Java是面向对象语言,你必须构造一个对象,这个对象的类需要有一个方法包含所需要的代码。然后传递一个对象。

    • lambda表达式是 Java语言 用来 **支持函数式编程 **的。

  • lambda表达式 表达的多样形式:

    • 参数,箭头(->)以及一个表达式,并包含显式的 return 语句

      (String first, String second) ->
      	{
        	if(first.length() < second.length()) return -1;
          	else if(first.length() > second.length()) return 1;
          	else return 0;
      	}
      
    • 没有参数也要提供 空括号

      () -> {for (int i = 100; i >= 0; i--) System.out.println(i);}
      
    • 如果可以推导出一个Lambda表达式的参数类型,则可以忽略其类型

      Comparator<String> comp = (first, second) // same as (String first, String second)
          		-> first.length() - second.length();
      
    • 如果方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号

      ActionListener listener = event ->
          System.out.println("The time is " 
            + Instant.ofEpochMilli(event.getWhen()));
      	// instead of (event) -> ... or (ActionEvent event) -> ...
      
    • 无需定义 lambda表达式 的返回类型,返回类型总是会由上下文推导得出。

      (String first, String second) -> first.length() < second.length())
      
  • 只有一个 抽象方法 的接口,需要这种接口对象时,可以提供 一个lambda表达式,这种接口称为函数式接口。这个表达式会转化为接口,例子如下:

    // 第二个参数 需要一个 Comparator实例(是只有一个方法的接口),可以提供一个 lambda表达式
    Arrays.sort(words, 
               (first, second) -> first.length() - second.length())
    
  • lambda表达式 可以转化为 接口 。eg:

    var timer = new Timer(100, event ->
        {
            System.out.println("At the tone, the time is" 
              + Instant.ofEpochMilli(event.getwhen()));
            Toolkit.getDefaultToolkit().beep();
        });
    
  • 最好把 lambda表达式 看做是一个函数,而不是一个对象,另外要接受 lambda表达式 可以传递到函数式接口

  • 实际上,在 Java中,对 lambda表达式 所能做的 也只是 转化为 函数式接口​。​没有为 ​Java​语言 ​增加​函数​类型。👿 👿 👿

  • 方法引用 : 例如下面的 System.out::println 就是一个方法引用,他指示 编译器 生成一个 函数式接口实例,用给定的方法覆盖实例的抽象方法并调用。 下面例子会生成 ActionListener对象, 它的 actionPerformed(ActionEvent event) 方法会调用 System.out.println(e)

    // 假设你希望只要出现一个定时器时间就打印这个事件对象
    var timer = new Timer(1000, event -> System.out.println(event));
    // 直接把 println方法 传递到 Timer构造器
    var timer = new Timer(1000, System.out::println);
    // 假设你想对字符串进行排序,而不考虑字母大小写。可以传递以下方法表达式
    Arrays.sort(strings, String::compareToIgnoreCase);
    
  • 方法引用lambda表达式 类似,两者都不是对象。在为 函数式接口 的变量赋值时会生成一个对象(即方法引用不能独立存在,总是会转化为函数式接口的实例)。注意: 只有当 lambda表达式 的体只调用一个方法而不做其他操作时,才能把 lambda表达式 重写为 方法引用(阐述了两者的区别)👿 👿 👿

  • 方法引用 主要有三种情况,要用 ::运算符 分割方法名与类名或对象

    • object::instanceMethod
      • 等价于 lambda表达式 eg: System.out::println 等价于 x -> System.out.println(x)
    • Class::instanceMethod
      • 第一个参数会成为方法的隐式参数 eg: String::compareToIgnoreCase 等价于 (x, y) -> x.compareToIgnoreCase(y)
    • Class::staticMethod
      • 所有参数都会传递到静态方法中 eg:Math.pow 等价于 (x, y) -> Math.pow(x, y)
  • 方法引用可以使用 this指参数,super也是合法的 eg: this::equals 等同于 x -> this.equals(x)

  • 构造器引用 : 与方法引用类似,只不用方法名为 new,eg: Person::new 是 person 构造器的一个引用

    ArrayList<String> names = ...;
    Stream<Person> steam = names.stream.map(Person::new);
    List<Person> people = stream.collect(Collectors.toList());
    
  • 可以用 数组类型 建立构造器引用。 eg: int::new 是一个构造器引用,他有一个参数:即数组的长度。这个引用等价于 lambda表达式 x -> new int[x]

  • 变量作用域 : 希望能够在 lambda表达式 中访问外围方法或类中的变量

    public static void repeatMessage(String text, int delay)
    {
        ActionListener listener = event ->
        {
            System.out,println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        new Timer(delay,listener).start();
    }
    
    • text 并不是在这个 lambda表达式 中定义的, lambda表达式 的代码可能会在 reapeatMessage 调用返回很久后才运行,而那时,这个参数变量 已经不存在了,如何保留呢?

      • lambda表达式 有 3 个部分:
        • 一个代码块
        • 参数
        • 自由变量的值,这是指非参数而且不在代码中定义的变量 eg: text
      • 这个 lambda表达式 的自由变量是 text。表示 lambda表达式 的数据结构必须会存储自由变量的值。我们说它被 lambda表达式 捕获(captured) eg:可以把 lambda表达式 转换为包含一个方法的对象,这个 自由变量 的值就会复制到这个对象的实例变量中。
    • 要确保 捕获的值是明确定义的,这里有一个重要限制:在 lambda表达式 中,只能引用 值不会改变 的变量(原因之一:在 lambda表达式 中更改变了,在并发执行多个操作会不安全)

      • 如果这个变量在 lambda表达式 外可能改变,这也是 不合法 的,
      • 捕获 的变量必须是 事实最终变量:这个变量在初始化之后就不会在为它赋新值
    • lambda表达式 体与嵌套块 有相同作用域。同样适用命名冲突和遮蔽的有关规则

      • lambda表达式 中声明一个局部同名的参数 或 局部变量 是 不合法 的。在一个方法中,不能有两个同名局部变量, lambda表达式 也不能有

        Path first = Path.of("/usr/bin");
        // ERROR: Variable first already defined
        Comparator<String> comp = 
            (first, second) -> first.length() - second.length();
        
  • lambda表达式 中使用 this关键字,是指创建 这个 lambda表达式 的方法的 this参数

    // 表达式 this.toString() 会调用 Application 对象的 toString 方法
    // 而不是 ActionListener实例的方法
    Public class Application{
        public void init(){
            ActionListener listener = event ->
            {
                System.out.println(this.toString());
                ...
            }
            ...
        }
    }
    
  • 编写 自定义方法 处理 lambda表达式

    • 使用 表达式的 重点: 延迟执行

    • 之所以希望以后再执行,有很多原因,如:

      • 在一个单独的线程中运行代码
      • 多次运行代码
      • 在算法的适当位置运行代码,eg:排序中的比较操作
      • 发生某种情况时运行代码,eg:点击一个按钮,数据传达
      • 只在必要时运行代码
    • 例子:假设你想要重复一个动作 n 次,将这个动作和重复次数传递到一个 repeat 方法

      repeat(10, () -> System.out.println("Hello,World!");
      // 接收 这个 lambda表达式 需要一个函数式接口
      public static void repeat(int n, Runnable action){
          for(int i = 0; i< n; i++) action.run();
      }
      
      // 如果我们希望告诉这个动作它出现在 哪一次迭代中,需要选择一个合适的函数式接口
      // 这个方法有一个int参数,且返回值为 void
      public interface IntConsumer{
          void accept(int value);
      }
      public static void repeat(int n, IntConsumer action){
          for(int i = 0; i < n; i++) action.accept(i);
      }
      repeat(10, i -> System.out.println("Countdown:" + (9 - i)));
      
  • 如果设计你自己的接口,其中只有一个抽象方法,可以用注解 @FunctionalInterface 来标记,如果你无意中增加了另一个抽象方法,编译器会产生一个错误消息

6.3 内部类


  • 内部类 : 定义在另一个类中的类

  • 为什么需要?

    • 内部类可以对同一个包中的其他类隐藏
    • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有属性
  • Java 中 内部类 与 C++ 中嵌套类有一定区别

    • Java 内部类的对象会有一个 隐式索引 ,指向实例化这个对象的外部类对象
    • Java 中静态内部类没有这个附加指针,相当于 C++ 中的嵌套类
  • 举个例子

    public class TalkingClock{
        private int interval;
        private boolean beep;
        
        public TalkingClock(int interval, boolean beep){ ... }
        public void start(){...}
        // an inner class
        
        public class TimerPrinter implememts ActionListener{
            ...
        }
    }
    
    public class TimerPrinter implememts ActionListener{
            public void actionPerformed(ActionEvent event){
                System.out.println("At the tone, this time is " +        
                   Instant.ifEpochMilli(event.getwhen));
                if(beep) Toolkit.getDefaultToolkit().beep();
            }
        }
    
    • 一个内部类方法 可以访问自身数据字段, 也可以访问创建它的外围类对象的数据字段
    • 内部类对象 总有一个 隐式引用, 指向创建它的外围类对象,它是不可见的,由 编译器 负责自动创建
  • 内部类有一个外围类的引用(隐式引用),使用外围类引用的正规语法: OutClass.this

    • 可以这样改写 TimerPrinter类 的 actionPerformed 方法:

       public void actionPerformed(ActionEvent event){
                  ...
                  if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
              }
      
  • 编写内部类对象构造器的明确语法: outerObject.new InnerClass(construction parameters)

    • 外围类引用 被设置为 内部类对象 的方法的 this引用, 通常 this限定词 多余

      ActionListener listener = this.new TimerPrinter();
      
    • 显示将 外围类引用 设置为其他的对象,因为对于公共内部类,任何 外部类对象 都可以构造 内部类对象

      var jabberer = new TalkingClock(1000, true);
      TalkingClock.TimePrinter listener = jabberer.new TimePrinter();
      
  • 外围类作用域之外,引用内部类: OuterClass.InnerClass

  • 内部类声明的 所有静态字段 都必须 是final,并初始化为一个编译时常量

  • Java语法规范限制:内部类不能有 static 方法

  • 内部类 是一个 编译器现象,与虚拟机无关。编译器会把内部类转化为 常规类文件, 用 $ 符分隔外部类名与内部类名,而虚拟机对此一无所知 eg: TimePrinter类 将被转化为成类文件 TalkingClock$TimePrinter.class ,内部类会存在 安全风险 👿 👿 👿

  • 局部内部类: 如果内部类只是在外围类中使用 一次,出现这种情况,可以在 **一个方法中局部地 **定义这个类

    public void start(int interval, boolean beep){
        class TimerPrinter implememts ActionListener{
            public void actionPerformed(ActionEvent event){
                System.out.println("At the tone, this time is " +        
                   Instant.ifEpochMilli(event.getwhen));
                if(beep) Toolkit.getDefaultToolkit().beep();
            }
            var listener = new TimerPrinter();
            var timer = new Timer(interval, listener);
            timer.start();
    }
    
    • 声明 局部内部类 不能有访问说明符
    • 作用域: 被限定在声明这个局部内部类的块中
    • 优势: 对 外部世界 完全隐藏,甚至 外围类 中的其他代码也不能访问它
    • 优点:他们不仅能够访问外部类的字段,还可以访问局部变量(必须是事实最终变量),在编译器底层会为这个局部变量创建相应实例字段,并把局部变量复制到构造器(防止方法结束,局部变量消失)
  • 匿名内部类: 只想创建这个类的一个对象,甚至不想要为类指定名字

    public void start(int interval, boolean beep){
            var listener = new ActionListener{
            public void actionPerformed(ActionEvent event){
                System.out.println("At the tone, this time is " +        
                   Instant.ifEpochMilli(event.getwhen));
                if(beep) Toolkit.getDefaultToolkit().beep();
            };
            var timer = new Timer(interval, listener);
            timer.start();
    }
    
    • 这个语法非常晦涩难懂, 含义:创建一个类的新对象,这个类实现了 ActionListener 接口,需要实现的方法在 actionPerformed 中定义
  • 语法:

    new SuperType(construction parameters){
        inner class methods and data
    }
    
    • SuperType 是 接口,如果是这样,内部类就要实现这个接口 eg: ActionListener

    • SuperType 是 类,如果是这样,内部类就要扩展这个类

    • 匿名内部类不能有构造器,因为构造器名字必须与类名相同,而匿名内部类没有类名 👿 👿 👿

    • construction parameters 构造参数 是要传递给超类构造器的变量,如果一个内部类实现一个接口,那就不会有任何构造参数( 接口是定义行为的,不是定义生产方式的 ),不过仍然要 提供一个小括号

      new InterfaceType(){
          methods and data
      }
      
    • 例子: 类构造新对象 和 扩展了那个类的匿名内部类对象 之间的差别

      // a Person Object
      var queen = new Person();
      // an object of an inner class extending Person
      var count = new Person("Dracula"){...};
      
      • 如果 构造参数列表 后跟一个开始大括号,就是定义 匿名内部类
    • 尽管不能有构造器,但可以提供一个对象初始化块

      var count = new Person("Dracula"){
          {Initialization}
          ...
      };
      
    • Java程序员习惯使用匿名内部类实现 事件监听器 和 其他回调,如今,还是使用 lambda表达式

      public void start(int interval, boolean beep){
              var timer = new Timer(interval, event -> {
                  System.out.println("At the tone, this time is " +        
                     Instant.ifEpochMilli(event.getwhen));
                  if(beep) Toolkit.getDefaultToolkit().beep();
              });
              timer.start();
      }
      
  • 静态内部类: 只是想为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。将内部类声明为 static ,这样就不会生成那个引用(外围对象引用)

  • 如果 内部类对象是 在静态方法中创建 ,那这个内部类 必须是静态内部类 👿 👿 👿

    • 原因: 静态方法不能访问对象,因为静态方法执行时,这个对象可能还没有创建,如果创建普通内部类,他会有外围类对象的引用,但是在实际执行这个静态方法的时候,编译器不知道是否已经创建了这个对象,所以,编译器不知道你产生外围类对象引用是否不合理,所以会报错
  • 在接口中声明的内部类自动是 static 和 public

6.4 服务加载器


  • ServiceLoader类

6.5 代理


  • 代理: 利用代理可以在 运行时 创建实现了一组给定接口的新类,只有在编译时期无法确定需要实现哪个接口时才有必要使用代理

目前不重要,留着以后看 page273

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值