Java基础之《Java核心卷1》第6章

6. 接口、lambda 表达式与内部类

6.1 接口

示例:Arrays 类中的sort方法承诺可以对对象数组进行排序, 但要求满足下列前提:对象所属的类必须实现了Comparable 接口

//Comparable接口
public interface Comparable{
    
    
	int compareTo(Object other) ;
}

//JavaSE 5.0中,Comparable 接口已经改进为泛型类型
public interface Comparable<T>{
    
    
	int compareTo(T other); // parameter has type T
}

任何实现Comparable 接口的类都需要包含compareTo方法

重点:

  • 接口中的所有方法自动地属于public(因此不必提供public关键字),实现它的类中必须声明为public
  • 实现接口时,必须重写所有的方法
  • 接口绝不能含有实例域,但可以包含常量,常量自动属于public static final
    interface Inter{
          
          
        public int a = 10;
    }
    //这里虽然看起来像普通实例域,但实际上是final类型,如果修改它会提示错误
    
  • 在JavaSE 8之前,也不能在接口中实现方法(现在可以实现(静态)方法)
  • 可以将接口看成是没有实例域的抽象类
  • 实现某个接口用关键字 implements
  • 接口不能被实例化,但可以被声明,如Comparable x;是可以的
    //Inter inter1 = new Inter();//编译错误,提示"Inter" is abstract; cannot be instantiated
    Inter inter2;//编译通过
    
  • instanceof 可以检测一个类是否实现了某个接口
  • 每个类只能有一个超类,但可以有多个接口(这也是为什么抽象类不能替代接口的原因)

实现接口的两种方法

//Employee实现Comparable接口,必须重写接口的所有方法:Comparable接口里只有一个compareTo方法
//方法1:普通实现,用强制类型转换,重写接口中的方法
class Employee implements Comparable{
    
    
    private int salary;
    @Override
    public int compareTo(Object otherObject) {
    
    
        Employee other = (Employee)otherObject;
        return Double.compare(salary, other.salary);
    }
}
//方法2:提供类型参数来重写接口中的方法(这是因为现在的Java已经支持泛型)
class Employee implements Comparable<Employee>{
    
    
    public int compareTo(Employee other){
    
    
        return Double.compare(salary, other.salary);
    }
}
//注意,compareTo返回的是一个int,如果返回值绝对值不超过(Integer_MAX_VALUE-1)/2就可以,否则应使用Integer.compare

反对称现象:
x.compareTo(y)成功,但y.compareTo()抛出异常ClassCastException
解决:
compare之前进行class的比较: if (getClass() != other.getClass() throw new ClassCastException();

默认方法:加default关键字

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

接口中一般不定义方法,但JDK 8之后允许定义默认方法
当接口被扩展时,新增的方法应当定义默认方法,否则原先实现它的类将无法工作 —— 保证扩展性

默认方法冲突
例如:接口和超类定义了同样的方法,出现了二义性
Java规定:(1)超类的优先级大于接口;(2)如果是实现的两个接口出现了同样方法,则报错,由程序员自己选择实现哪个,即调用时 接口名.super.方法名

interface face1{
    
    
    default int func(){
    
    return 1;}
}
interface face2{
    
    
    default int func(){
    
    return 2;}
}
//实现的多个接口中有相同方法时,必须重写这个相同方法
//实现的多个接口中没有相同方法时,可以不用重写
//使用时必须用接口名.super.方法名的形式调用
class Cls implements face1, face2{
    
    
    public int func(){
    
    
        System.out.println(face2.super.func());
        return 2;
    }
}

super和this在接口中的应用
接口名.super 指向的不是接口的父接口,而是其本身
接口.this 的使用方法是错误的

6.2 接口示例

接口和回调

实现回调:定时器方法
定时器需要知道调用哪一个方法,并要求传递的对象所属的类实现了java.awt.event包的 ActionListener接口

public interface ActionListener
	void actionPerformed(ActionEvent event);
}
  • 示例:每隔10秒打印一次信息
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

class TimePrinter implements ActionListener{
    
    
    public void actionPerformed(ActionEvent event){
    
    
        System.out.println(new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}
public class Test{
    
    
    public static void main(String[] args){
    
    
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(2000, listener);
        t.start();
        //while(true){}
        JOptionPane.showMessageDialog(null, "Quit");//也是为了保持程序运行,必须加一句才能看到定时器效果
        System.exit(0);
    }
}
  • API
//javax.swing.JOptionPane 1.2
static void showMessageDialog(Component parent, Object message);//显示一个包含一条消息和OK按钮的对话框

//javax.swing.Timer 1.2
Timer(int interval, ActionListener listener);//构造一个定时器, 每隔interval毫秒通告listener—次
void start();//启动定时器一旦启动成功,定时器将调用监听器的actionPerformed
void stop();//停止定时器。一旦停止成功,定时器将不再调用监听器的actionPerformed

//java.awt.Toolkit 1.0
static Toolkit getDefaultToolkit();//获得默认的工具箱。工具箱包含有关GUI环境的信息
void beep();//发出一声铃响。

Comparator接口

之前的comparable接口用于按大小排序等(实现了compareTo方法,能够应用sort函数来排序)
Comparatot接口可以更加灵活地进行排序(例如从大到小,字符长度等自定义方式)

  • comparator

    public interface Comparator<T>{
          
          
    	int compare(T first, T second);
    }
    
  • 按字符串长度对数组排序

    class LengthComparator implements Comparator<String>{
          
          
        @Override
        public int compare(String first, String secnod){
          
          
            return first.length() - secnod.length();
            //return first.compareTo(secnod);//按字典大小排序
        }
        //注意,如果没有加<String>,这里的String要写成Object
    }
    public class Test{
          
          
        public static void main(String[] args){
          
          
            String[] friends = {
          
           "Peter", "Paul", "Say" };
            Arrays.sort(friends, new LengthComparator());
            System.out.println(Arrays.toString(friends));
        }
    }
    

Clonable接口 对象克隆

obj.clone() 是Object的一个protected方法,对象不能直接调用clone

关于protected:
(1)如果是在是一个包中,可以访问
(2)不同包中,需要子类重写了protected修饰的方法,这样子类对象才能使用;否则,不能使用;
即子类对象不能调用父类的protected方法,但可以调用子类重写的protected方法

clone不能完成对象的子对象的clone,也就是说,尽管通过clone两个对象是深拷贝,但它们的子对象仍共享空间;
这种情况需要重写clone()方法

  • 重写clone,但子对象仍共享

    class Animal{
          
          
    	private int size;
    	public void setSize(int size){
          
           this.size = size; }
    	public int getSize(){
          
           return this.size; }
    }
    class Person implements Cloneable{
          
          
        private int age;
        Animal animal = new Animal();
        public Person(int age, int size){
          
          
            this.age = age;
            this.animal.setSize(size);
        }
        public void setAge(int age){
          
           this.age = age; };
        public int getAge(){
          
           return this.age; }
        /**
         * 以下为重点
         * !!必须重写clone函数,不然调用person.clone()报错clone() has protected access
         */
        public Person clone() throws CloneNotSupportedException{
          
          
            return (Person)super.clone();
        }
    }	
    public class Test {
          
          
        public static void main(String[] args) throws CloneNotSupportedException {
          
          
            Person person = new Person(10, 11);
            Person copy = person.clone();
            copy.setAge(21);
            copy.animal.setSize(22);
            System.out.println(person.getAge()+" "+person.animal.getSize());//输出:10,22
            System.out.println(copy.getAge()+" "+copy.animal.getSize());//输出:21,22
            /**
             * 上面的写法将导致animal是浅拷贝,即copy.animal.setSize(22);同时导致原对象和拷贝对象都被改变
             * 需要对所有子对象也进行一次深拷贝
             */
        }
    }
    
  • 重写clone,实现完全的深拷贝——在每个涉及到的类中都要重写clone()方法

    //在子对象的类中重写clone
    class Animal implements Cloneable{
          
          
        ...
        public Animal clone() throws CloneNotSupportedException{
          
          
            return (Animal) super.clone();
        }
        ...
    }
    //在类中要对所有子对象进行一次clone()
    class Person implements Cloneable{
          
          
        ...
        public Person clone() throws CloneNotSupportedException{
          
          
            Person cloned = (Person) super.clone();
            cloned.animal = (Animal) animal.clone();
            return cloned;
        }
        ...
    }
    //最终可以看到输出结果,对象和克隆对象已互不影响了
    

    抛出异常一是在方法名后加上 throw … ,二是使用try…catch
    后者一般适用于不怎么被继承的类,前者可以使它的子类选择抛出异常

6.3 lambda表达式

前面的Timer和Comparator示例中,Timer接受了一个对象 new TimePrinter(),Comparator也接受了一个对象 new LengthComparator(),这实质上是在传递代码块,这个代码块会在某个时刻被调用执行

java是面向对象编程,因此要将代码段封装在一个类的一个方法中,才能进行传递(其他语言可以直接处理代码——函数指针)

lambda表达式可以处理代码块的传递

函数式接口

Java中已经有很多封装代码块的接口,如 ActionListener或Comparator。lambda表达式与这些接口是兼容的

对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口
任何有一个抽象方法的接口都是函数式接口

例如,lambda可以赋值给Comparator,但不能赋值给Object,因为Object不是一个函数式接口

list.removelf(e -> e == null);//从一个数组列表种删除所有的null值
  • 示例1:比较长度

    (String first, String second) -> {
          
          
        if(first.length() < second.length()) return -1;
        else if(first.length() > second.length()) return 1;
        else return 0;
    }
    
  • 示例2:没有参数的lambda表达式

    () -> {
          
          
        for (int i = 0; i < 10; ++i) System.out.println(i);
    };
    
  • 示例3:省略参数类型

    Comparator<String> comp = (first, second) -> first.length() - second.length();
    

    例如前面按长度为字符串数组排序的原始写法是:

    class LengthComparator implements Comparator<String>{
          
          
    	@Override
    	public int compare(String first, String secnod){
          
          
             return first.length() - secnod.length();
             //return first.compareTo(secnod);//按字典大小排序
      	}
         //注意,如果没有加<String>,这里的String要写成Object
    }
    

    使用lambda表达式改写后为

    public class Test{
          
          
        public static void main(String[] args){
          
          
            String[] friends = {
          
           "Peter", "Paul", "Say" };
            //第1种写法:Comparator(使用的接口)去接
            Comparator<String> comp = (first, second) -> first.length() - second.length();
            Arrays.sort(friends, comp);
            //第2种写法:lambda表达式可以自动推导返回值类型
            Arrays.sort(friends, (first, second) -> first.length() - second.length());
            System.out.println(Arrays.toString(friends));
        }
    }
    
  • 示例4:省略小括号:仅有1个参数

    ActionListener listener = event -> System.out.println(new Date());
    

    前面的回调函数就可以改成为:

    public class Test{
          
          
        public static void main(String[] args){
          
          
            ActionListener listener = event -> System.out.println(new Date());
            Timer t = new Timer(2000, listener);
            t.start();
            JOptionPane.showMessageDialog(null, "Quit");//也是为了保持程序运行,必须加一句才能看到定时器效果
            System.exit(0);
        }
    }
    

无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出

方法引用和lambda

Timer t = new Timer(1000, event -> System.out.println(event));

上述语句还可以改进为,将println方法直接传递给Timer构造器,如:

Timer t = new Timer(1000, System.out::println);

表达式System.out::println 是一个方法引用( method reference ),它等价于lambda表达式 x -> System.out.println(x)

如果要对字符串数组排序,并忽略大小写,可以写成:

Arrays.sort(strings, String::compareToIgnoreCase);

:: 主要有三种情况

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

前2种情况,方法引用等价于lambda表达式,例如 Math::pow 等价于 (x, y) -> {Math.pow(x, y)}
第3种情况,第1个参数会成为方法的目标。例如,String:.compareToIgnoreCase等同于(x, y) -> x.compareTolgnoreCase(y)。

this 和 super:
this::equals 等价于 x -> this.equals(x)
super::instanceMethod,调用方法的超类版本

构造器引用

参考:https://blog.csdn.net/u011863822/article/details/121427637

什么是构造器引用
与函数式接口相结合,自动与函数式接口中方法兼容
可以把构造器引用赋值给定义的方法

和方法引用类似,只是方法名变为new,如Person::new

  • 无参构造器引用
//必须是函数式接口接口
interface Supplier<T>{
    
    
    T get();
}
//Person类
class Person{
    
    
    String name;
    public Person(){
    
    
        System.out.println("这是无参构造");
    }
    public Person(String name){
    
    
        this.name = name;
        System.out.println("这是有参构造");
    }
}
//三种调用无参构造的方式
//1. 普通方式
//注意:接口是不能被new的,这里实际上是生成了一个匿名内部类
Supplier<Person> person1 = new Supplier<Person>() {
    
    
    @Override
    public Person get() {
    
    
        return new Person();
    }
};
person1.get();
//2. lambda方式
Supplier<Person> person2 = ()->new Person();
person2.get();
//3. 构造器引用
Supplier<Person> person3 = Person::new;
person2.get();
  • 有参构造器引用
//函数式接口
interface Provided<T1, T2>{
    
    
    T2 apply(T1 t);
}
//Person类,同上
//三种调用有参构造的方式
//1. 普通方式
Provided<String, Person> person4 = new Provided<String, Person>() {
    
    
    @Override
    public Person apply(String t) {
    
    
        return new Person(t);
    }
};
person4.apply("zhangsan");
//2. lambda方式
Provided<String, Person> person5 = (t)->new Person(t);
person5.apply("zhangsan");
//3. 构造器引用
Provided<String, Person> person6 = Person::new;//和前两种方法不同,有参无参的这一句是一样的
person6.apply("zhangsan");

应用场景尚且不了解

泛型数组构造器

Java中禁止创造泛型数组

  • 共变数组
    类型为T的数组可以包括子类型S的元素,如 Number[] 中可以包含 Integer元素

    • 编译通过,运行报错

      Integer[] arrayInt = {
              
              1,2,3};
      Number[] arrayNum = arrayInt;
      arrayNum[0] = 3.14;
      /**
      * 报错:ArrayStoreException
      * 理解一下,arrayInt在堆上开辟了3个Integer类型的空间,arrayNum只是一个对象变量,也引用到这3个Integer空间上
      * 编译时符合共变数组的规则,不报错;运行赋值时,发现将一个Double类型赋值到了Integer空间,报错
      */
      
    • 编译和运行都通过

      Number[] numbers = new Number[3];
      numbers[0] = new Integer(10);
      numbers[1] = new Double(3.14);
      numbers[2] = new Byte((byte) 0);
      
      numbers[0] = 3.13;//不报错
      numbers[2] = new Double(3.15);//不报错
      
  • 同一类型的不同维度数组的Class是不一样的

    Integer a = 10;//自动装箱
    System.out.println(a.getClass().getName());//输出:java.lang.Integer
    Integer[] aa = new Integer[10];
    System.out.println(aa.getClass().getName());//输出:[Ljava.lang.Integer;
    Integer[][] aaa = new Integer[10][10];
    System.out.println(aaa.getClass().getName());//输出:[[Ljava.lang.Integer;
    //可以看到,同一类型的不同数组的Class是不一样的
    //同一类型的对象的Class是相同的
    
  • 类型擦除

    1. String[]Object[] 的subtype
    2.Stack<String> 不是 Stack<Object> 的subtype,泛型没有共变性
    

    因为类型擦除,泛型类型在运行期间,Stack< String> 和 Stack< Object> 类型会被擦除成Stack
    所有的Stack泛型类型分享同一个Stack.class

  • 为什么禁止泛型数组 —— 因为类型擦除

    (1)假设虚拟机对数组没有任何额外的限制,就像处理普通类那样:那么由于Object是所有类的父类,只需要将任意数组先转换为Object[],那么就可以往里面放任意类型的数据,这显然是非常危险的

    (2)于是为了保证类型安全,Java数组进行规定:必须明确知道数组内部元素的类型,并且记住这个类型,每次往数组里面插入新元素时都要进行类型检查,不匹配就抛出异常

    (3)数组的 “记住元素类型” 和 泛型要求的 “类型擦除” 本身就相互矛盾,因此禁止了泛型数组

  • 实现泛型数组

    • 非数组构造器引用

      ArrayList<String> names = new ArrayList<>();
      names.add("first");//调用有参构造,给name赋值
      names.add("second");
      names.add("third");
      Stream<Person> stream = names.stream().map(Person::new);//Stream是一个函数式接口
      List<Person> people = stream.collect(Collectors.toList());
      System.out.println(people.get(0).getName());
      
    • 泛型数组

      //Person[]
      ArrayList<String> names = new ArrayList<>();
      names.add("first");//调用有参构造,给name赋值
      names.add("second");
      names.add("third");
      Stream<Person> stream = names.stream().map(Person::new);
      Person[] people = stream.toArray(Person[]::new);
      System.out.println(people[0].getName());
      
      //如果想从Person类,变为Student类,只需要
      Stream<Student> stream = names.stream().map(Student::new);
      Student[] students = stream.toArray(Student[]::new);
      System.out.println(students[0].getName());
      
    • 泛型数组2

      //函数式接口
      interface Provided<T1, T2>{
              
              
          T2 apply(T1 t);
      }
      
      // 方法1:传统方式
      Provided<Integer, String[]> array1 = new Provided<Integer, String[]>() {
              
              
          @Override
          public String[] apply(Integer t) {
              
              
              return new String[t];
          }
      };
      String[] strings1 = array1.apply(10);
      strings1[0] = "first";
      System.out.println(Arrays.toString(strings1));
      
      //方法2:lambda
      Provided<Integer, String[]> array2 = t ->{
              
              return new String[t];};
      String[] strings2 = array1.apply(10);
      strings2[0] = "second";
      System.out.println(Arrays.toString(strings2));
      
      //方法3:构造方式引用
      Provided<Integer, String[]> array3 = String[]::new;
      String[] strings3 = array1.apply(10);
      strings3[0] = "third";
      System.out.println(Arrays.toString(strings3));
      //改为int型
      Provided<Integer, int[]> array4 =int[]::new;
      int[] int4 = array4.apply(10);
      int4[0] = 22;
      System.out.println(Arrays.toString(int4));
      
      

      作用类似下面的代码,但下面的代码会报错

      class faxing<T>{
              
              
          public T[] apply(int t){
              
              
              return new T[t];//报错!提示T不能被new
          }
      }
      

lambda中的变量作用域

先看代码:

public static void main(String[] args){
    
    
    repeatMessage("blingbling", 2000);
}
public static void repeatMessage(String text, int delay) {
    
    
    ActionListener listener = event -> {
    
    
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
    JOptionPane.showMessageDialog(null, "Quit");
    System.exit(0);
}

lambda表达式有3个部分

  • 一个代码块:即 event-> 后面的 {} 部分
  • 参数:即event
  • 自由变量的值:指非参数而且不在代码中定义的变量,如text
    lambda表达式将要存储自由变量的值,相当于复制了一下
    称自由变量是被lambda表达式 捕获(captured)

在Java中,lambda表达式就是闭包(closure)

lambda 表达式可以捕获外围作用域中变量的值:
!在lambda表达式中,不能改变自由变量的值
!也不能在lambda表达式外部更改自由变量的值
即在repeatMessage可以接触到的作用域内,都不能更改text的值(编译通过,运行报错)
从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量

lambda表达式的参数在当前作用域内不能有同名变量

lambda表达式中的 this
this指的是包含lambda表达式的方法,而不是接受lambda表达式的类(注意static方法中没有this)
例如上例中,如果lambda内部有this,指的是repeatMessage,而不是ActionListener

处理lambda表达式

在这里插入图片描述

lambda表达式的重点是延迟执行:当在一个单独的线程中运行代码或者在合适的时候再运行代码时需要

不传参的lambda表达式

public static void repeat(int n, Runnable action){
    
    
    for(int i=0; i<n; ++i) action.run();
}
public static void main(String[] args){
    
    
    repeat(10, ()->System.out.println("haha"));
}
  • 要用一个函数式接口接lambda表达式参数,如Runnable
  • 运行时使用 action.run() 语句,它会执行lambda的主体

传参的lambda表达式

希望打印当前i的值

interface intConsumer{
    
    
    void accept(int i);
}
public class Test{
    
    
    public static void repeat(int n, intConsumer action){
    
    
        for(int i=0; i<n; ++i) action.accept(i);//将i传给lambda表达式
    }
    public static void main(String[] args){
    
    
        repeat(10, i -> System.out.println(9-i));
    }
}

大多数标准函数式接口都提供了非抽象方法来生成或合并函数
Predicate.isEqual(a).or(Predicate.isEqual(b)) 就等同于x -> a.equals(x) || b.equals(x)

@FunctionalInterface注释,自定义接口,只有一个抽象方法:
如果增加一个非抽象方法, 编译器会产生一个错误消息;另外javadoc 页里会指出你的接口是一个函数式接口

再谈Comparator

按名字对Person进行排序

Arrays.sort(people, Comparator.comparing(Person::getName));

thenComparing:将比较器连接起来

//名字相同时,再按照firstName排序
Arrays.sort(people, Comparator.comparing(Person::getName).thenComparing(Persin::getFirstName));

变体

//根据名字长度排序
Arrays.sort(people, Comparator.comparing(Person::getName, (s, t)->Integer.compare(s.length, t.length())));
//等价于
Arrays.sort(people, Comparator.comparingInt(p->p.getName().length()));

nullsFirst,nullsLast
利用上面的代码,如果getName()返回值为null,将报错
nullsFirst,nullsLast静态方法会修改现有的比较器, 从而在遇到null 值时不会抛出异常, 而是将这个值标记为小于或大于正常值

//假设一个人没有中名时getMiddleName会返回一个null, 可以使用
Comparator.comparing(Person::getMiddleName(), Comparator.nullsFirst(...));
  • nullsFirst:null被放在前面;nullsLast:null被放在后面
  • naturalOrder():非null值按字典序排列;reverseOrder():非null值反序排列
Person[] persons = new Person[3];
persons[0] = new Person("shell");//有参构造设置middleName
persons[1] = new Person();
persons[2] = new Person("aiden");
//Arrays.sort(persons, Comparator.comparing(Person::getMiddleName));//报错:NullPointerException
Arrays.sort(persons, Comparator.comparing(Person::getMiddleName, 
                                          Comparator.nullsFirst(Comparator.naturalOrder())));
//或者是自定义排序:按名字长度排序,null值放在前面
Arrays.sort(person, Comparator.comparing(Person::getMiddleName, 
										Comparator.nullsFirst((s, t)->t.length()-s.length())));
//Arrays.sort(persons, Comparator.comparing(Person::getMiddleName, 
//                                          Comparator.nullsFirst(Comparator.reverseOrder())));
//Arrays.sort(persons, Comparator.comparing(Person::getMiddleName,
//                                 Comparator.nullsFirst(Comparator.naturalOrder())).reversed());

6.4 内部类

内部类

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

示例

//之前的定时器程序,改为内部类的编写方式
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;
class TalkingClock{
    
    
    private int interval;
    private boolean beep;
    public TalkingClock(int interval, boolean beep) {
    
    
        this.interval = interval;
        this.beep = beep;
    }
    public void start() {
    
    
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(interval, listener);
        t.start();
    }
    //主要:内部类
    public class TimePrinter implements ActionListener {
    
    
        public void actionPerformed(ActionEvent event){
    
    
            System.out.println("At the tone, the time is " + new Date());
            if(beep) Toolkit.getDefaultToolkit().beep();
        }
    }
}
public class Test{
    
    
    public static void main(String[] args){
    
    
        TalkingClock clock = new TalkingClock(1000, true) ;
        clock.start();      
        JOptionPane.showMessageDialog(null, "Quit program?");// keep program running until user selects "0k"
        System.exit(0);
    }
}

重点说明:

  • outer引用

    beep变量:内部类TimePrinter类没有实例域或者名为beep的变量,引用的是TalkingClock对象的域
    outer引用外部类:outer是一个隐式引用,不是关键字,上面的 if(beep) 实际上是 if(outer.beep)

    if(beep) 其实是 if(outer.beep)
    
  • 内部类的构造器

    编译器修改了所有的内部类的构造器,生成一个默认的构造器,如

    public TimePrinter(TalkingClock clock){
          
           // automatically generated code
    	outer = clock;
    }
    
  • 内部类的特殊语法规则

    • 非静态内部类,使用时只能通过外部类的对象:对象.new
    • 静态内部类,使用时可以通过外部类类名:类名.new
    class Outer{
          
          
    	public Outer(){
          
          }
    	class Inner{
          
          } //非静态内部类
    	static class StaticInner{
          
          } //静态内部类
    	
    	public void test(){
          
          
          //1. 外部类引用内部类
          Inner inner1 = new Inner();
          //2. 外部类引用内部类
          Inner inner2 = this.new Inner();//这里的this也是指向一个Outer对象,即本身
        }
    }
    public class Test{
          
          
    	public static void main(String[] args){
          
          
        //3. 实例化外部类
        Outer outer =  new Outer();
          
        //4. 只能通过对象定义内部类:对象.new;不能通过类.new
        Outer.Inner inner = outer.new Inner();
        //Outer.Inner inner = new Outer.Inner();
    
        //5. 当内部类为静态内部类时,可以使用:类.new,不能通过对象.new
        Outer.StaticInner staticInner = new Outer.StaticInner();
        //Outer.StaticInner staticInner = new outer.StaticInner();//报错
    }
    
  • 内部类的final和static

    内部类中声明的所有静态域都必须是final

    class Outer {
          
          
    	public Outer() {
          
          }
        class Inner {
          
          
            //private static int a = 1;//报错
            private static final int b = 2;
        }
    }
    //仅针对非静态内部类,静态内部类中没有这个限制
    

    内部类不能有static方法 —— 可以定义static方法,但它只能访问外围类的静态域和方法

  • 内部类的编译

    编译器用 $ 分隔外部类名与内部类名,如:

    TalkingClock$TimePrinter.class
    //UNIX命令行使用innerClass.TalkingClock\$TimePrinter,反转义才能正常执行
    
  • 内部类的安全问题

    在虚拟机中不存在私有类, 因此编译器将会利用私有构造器生成一个包可见的类(即编写时的内部类)

    假设定义TalkingClock和TimePrinter两个类,它们暂时毫无关系(TimePrinter非TalkingClock内部类)
    在TimePrinter的构造器内手动加上:

    private TalkingClock outer;
    public TimePrinter(TalkingClock clock){
          
          
    	outer = clock;
    }
    

    此时,再在TimePrinter中调用TalkingClock的beep,发现报错,这是因为:
    内部类的访问权限比常规类更大

    生成内部类时,会在外部类中生成一个类似 access$0() 的方法,这个方法的存在使得内部类可以访问外部类的私有域
    手动加上的outer不会生成该方法,因此报错。access会导致安全问题,但这种攻击是很难的(需要获得包可见性)

局部内部类

对于那些只使用一次的内部类,可以采用局部内部类,如

class TalkingClock{
    
    
    private int interval;
    private boolean beep;
    public TalkingClock(int interval, boolean beep) {
    
    
        this.interval = interval;
        this.beep = beep;
    }
    public void start(){
    
    
        //!!!局部内部类
        class TimePrinter implements ActionListener{
    
    
            public void actionPerformed(ActionEvent event){
    
    
                System.out.println("At the tone, the tine is " + new Date());
                if (beep) Toolkit.getDefaultToolkit().beep();
            }
        }
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(interval, listener);
        t.start();
    }
}
//调用方式
TalkingClock t = new TalkingClock(2000, true);
t.start();
JOptionPane.showMessageDialog(null, "Quit program?");

局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块(start方法)内
提供了更好的隐藏性:即便是包含start方法的类,也不能访问局部类的数据

再考虑以下程序

class TalkingClock{
    
    
    public void start(int interval, boolean beep){
    
    
        //局部内部类
        class TimePrinter implements ActionListener{
    
    
            public void actionPerformed(ActionEvent event){
    
    
                System.out.println("At the tone, the tine is " + new Date());
                if (beep) Toolkit.getDefaultToolkit().beep();
            }
        }
        ActionListener listener = new TimePrinter();
        Timer t = new Timer(interval, listener);
        t.start();
    }
}
//调用方式
TalkingClock t = new TalkingClock();
t.start(2000, true);
JOptionPane.showMessageDialog(null, "Quit program?");

此时,外部类就不用再定义interval和beep变量了。看上去内部类中使用的是start()方法中的参数,但实际上并非这样简单
上面程序的运行流程是:

1. 调用start方法
2. 调用内部类TimePrinter的构造器,初始化对象变量listener
3. 将listener引用传递给Timer构造器,定时器开始计时,start方法结束。此时,start方法的beep参数变量不复存在  
4. 计时器时间一到,actionPerformed方法要执行 if(beep) —— 理论上会出错,因为此时beep已被释放

编译器为局部内部类构造了名字TalkingClock$TimePrinter,并在这个局部类中对beep进行备份,因此该程序才能工作
运行时的代码为:

class TalkingClock$TimePrinter{
    
    
    TalkingClock$TimePrinter(TalkingClock, boolean);
    public void actionPerformed(java.awt.event.ActionEvent);
    final boolean val$beep;//必须是final
    final TalkingClock this$0;
}

final和局部变量

在JavaSE 8之前, 必须把从局部类访问的局部变量声明为final,如
public void start(int interval , final boolean beep)

如果想要在封闭作用域内更新局部变量,将报错,如下:

int counter = 0;
Date[] dates = new Date[100];
for (int i = 0; i < dates.length; i++)
    dates[i] = new Date(){
    
    
    public int compareTo(Date other){
    
    
        counter++; //Error,提示:java: 从内部类引用的本地变量必须是最终变量或实际上的最终变量
        return super.compareTo(other);
    }
};

修改方法:使用一个长度为1的数组,如

int[] counter = new int[1];
Date[] dates = new Date[100];
for (int i = 0; i < dates.length; i++)
    dates[i] = new Date(){
    
    
    public int compareTo(Date other){
    
    
        counter[0]++;
        return super.compareTo(other);
    }
};

原理:将局部变量变成 对象 / 数组

匿名内部类

只创建这个类的一个对象,就不必命名了,称为匿名内部类

//创建一个实现ActionListener接口的类的新对象,需要实现的方法actionPerformed定义在括号内
public void start(int interval, boolean beep){
    
    
	ActionListener listener = new ActionListener(){
    
    //匿名内部类,可以和局部内部类对比一下,省略类名,用相应接口来实现
        public void actionPerformed(ActionEvent event){
    
    
            System.out.println("At the tone, the time is " + new Date());
            if(beep) Toolkit.getDefaultToolkit().beep();
        }
    };
    Timer t = new Timer(interval, listener);
    t.start();
}

这里的是ActionListener接口,匿名内部类会自动 implement 它
也可以是一个类,这样匿名内部类会自动 extends 它

匿名内部类没有构造器,构造器参数会传递给超类的构造器;对于匿名内部类实现接口时,更是不允许有构造器参数

Person queen = new Person("Mary");//得到普通类对象
Person count = new Person("Dracula") {
    
    ...};//得到实现了Person类的匿名内部类的对象

很多程序员习惯用匿名内部类实现事件监听和回调,但使用lambda表达式来实现会更简洁

  • 技巧:双括号初始化

    //传递一个ArrayList数组
    ArrayList<String> friends = new ArrayList();
    friends.add("Harry");
    friends.add("Tony");
    invite(friends);
    //使用匿名列表代替上述代码
    invite(new ArrayList<String>(){
          
          {
          
          add("Harry"); add("Tony")}});
    
  • 匿名子类和equals

    if (getClass() != other.getClass()) return false;//此测试条件用于匿名子类时无效
    //因为匿名类名改变了
    

    测试

    interface MyInterface {
          
          
        public abstract void myCompare(MyInterface other);
    }
    public class Test{
          
          
        public static void main(String[] args){
          
          
            MyInterface m1 = new MyInterface() {
          
          
                @Override
                public void myCompare(MyInterface other) {
          
          
                    if(getClass() == other.getClass()) 
                        System.out.println(getClass().getName()+"=="+other.getClass().getName());
                    else 
                        System.out.println(getClass().getName()+"!="+other.getClass().getName());
                }
            };
            MyInterface m2 = new MyInterface() {
          
          
                @Override
                public void myCompare(MyInterface other) {
          
          
                    if(getClass() == other.getClass()) 
                        System.out.println(getClass().getName()+"=="+other.getClass().getName());
                    else 
                        System.out.println(getClass().getName()+" "+other.getClass().getName());
                }
            };
            //m1和m2不相等,因为它们的class分别是pack.Test$1 pack.Test$2
            m1.myCompare(m2);
            //m1和m3相等,都是pack.Test$1
            MyInterface m3 = m1;
            m1.myCompare(m3);
            //使用m1.getClass().newInstance();也可以得到和m1同样类型的对象
        }
    }
    
  • 静态方法的类名

    普通方法的类名:getClass(),实际上它是this.getClass
    静态方法的类名:静态方法没有this,因此用如下方法

    new Object(){
          
          }.getClass().getEnclosingClass();
    //newObject(){} 会建立Object 的一个匿名子类的一个匿名对象
    //getEnclosingClass则得到其外围类
    
    public class Test{
          
          
        public static void main(String[] args) {
          
          
            System.out.println(new Object(){
          
          }.getClass());//输出:class pack.Test$1
            System.out.println(new Object(){
          
          }.getClass().getEnclosingClass());//输出:class pack.Test
    	}
    }
    

静态内部类

用static修饰的内部类,只有内部类可以声明为static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所有内部类完全一样

class Outer {
    
    
    class Inner {
    
    }
    static class StaticInner {
    
    }
}
//内部类对象
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();//需要借助outer这个外部类对象才能new
Outer.StaticInner staticInner = new Outer.StaticInner();//不需要借助外部类对象

作用
一般的内部类对象是以外部类对象的存在为前提的
静态内部类和外部类没有关系,只是借用外部类隐藏自己

适用情况:在内部类不需要访问外围类对象的时候, 应该使用静态内部类

特点
与常规内部类不同, 静态内部类可以有静态域和方法
声明在接口中的内部类自动成为static和public类

注意
静态内部类里面只能访问外部类中的静态方法与变量,这是由于静态内部类和外部类是相对独立的
如果要访问非静态方法和变量,则需要用外部类的对象来访问

  • 示例,一次遍历得到最大值和最小值

    /**
     * 考虑类的话:一般的实现是在类中定义一个getMax()和getMin(),遍历两次
     * 使用静态内部类,可以一次遍历得到
     */
    class ArrayAlg{
          
          
        //定义静态内部类
        public static class Pair{
          
          
            private double max;
            private double min;
    
            public Pair(double max, double min){
          
          
                this.max = max;
                this.min = min;
            }
            public double getMax(){
          
          
                return this.max;
            }
            public double getMin(){
          
          
                return this.min;
            }
        }
        //定义一个静态方法,返回Pair类对象
        public static Pair minmax(double[] value){
          
          
            double min = Double.POSITIVE_INFINITY;
            double max = Double.NEGATIVE_INFINITY;
            for(double v:value){
          
          
                if(min > v) min = v;
                if(max < v) max = v;
            }
            return new Pair(max, min);
            //return new ArrayAlg().new Pair(max, min);//如果Pair是非静态内部类,则需要这样做
        }
    }
    public class Test{
          
          
        public static void main(String[] args){
          
          
            double[] d = new double[5];
            for(int i=0; i<d.length; ++i) d[i] = 100*Math.random();
            ArrayAlg.Pair p = ArrayAlg.minmax(d);
            System.out.println(Arrays.toString(d));
            System.out.println("max="+p.getMax()+", min="+p.getMin());
        }
    }
    

6.5 代理

代理的目的:想使用某个类的方法,但需要在该方法中增加一些操作(即目标对象的增强)
实现:在代理类中重写一个方法,内容为:增加的操作+调用原方法

原博客:https://blog.csdn.net/justloveyou_/article/details/74203025

  • 代理对象存在的价值主要用于拦截对真实业务对象的访问
  • 代理对象应该具有和目标对象(真实业务对象)相同的方法,即实现共同的接口或继承于同一个类
  • 代理对象应该是目标对象的增强(不修改原有代码)

例如,在项目开发中没有加入缓冲、日志这些功能而后期需要加入,就可以使用代理来实现,不必去直接修改已经封装好的目标类

静态代理

(1)抽象接口:首先得有一个接口,通用的接口是代理模式实现的基础

public interface Movie{
    
    
    void play();
}

(2)被代理角色(目标类)

public class RealMovie implements Movie {
    
    
    @Override
    public void play() {
    
    
        System.out.println("您正在观看电影!");
    }
}

(3)代理角色(代理类)

代理类要实现同样接口

public class Cinema implements Movie {
    
    
    RealMovie movie;
    public Cinema(RealMovie movie) {
    
    
        super();
        this.movie = movie;
    }
    @Override
    public void play() {
    
    
        guanggao(true);    // 代理类的增强处理
        movie.play();     // 代理类把具体业务委托给目标类,并没有直接实现
        guanggao(false);    // 代理类的增强处理
    }
    public void guanggao(boolean isStart){
    
    
        if(isStart) System.out.println("片头的广告!");
        else System.out.println("片尾的广告!");
    }
}

(4)客户端

RealMovie realmovie = new RealMovie();
Movie movie = new Cinema(realmovie);//用接口去接,将目标类传给代理类
movie.play();//可以实现增强的功能

动态代理

(1)抽象接口

public interface Subject {
    
    
    public Object doSomething();
}

(2)被代理角色(目标类)

public class RealSubject implements Subject {
    
    
    public Object doSomething() {
    
    
        System.out.println("call doSomething()");
        return null;
    }
}

(3)代理角色(代理类)

在动态代理中,代理类及其实例是程序自动生成的,因此我们不需要手动去创建代理类
Proxy通过使用InvocationHandler对象生成具体的代理对象

/**
* invoke方法:通过反射的机制,可以通过invoke方法来调用类的函数
* invoke方法的第一个参数是一个对象。此对象可以为:1.方法持有者;2.方法持有者的继承者。如果该方法是静态方法,可以用null或者用类来代替
* 第二个参数是变长的,是调用该方法的参数。
*/
//代理需要实现InvocationHandler接口,并重写invoke方法
class ProxyHandler implements InvocationHandler{
    
    //注意,实现的是InvocationHandler,而不是Subject
    private Object proxied; // 被代理对象,注意:无论被代理对象是什么类型,这里都写Object
    public ProxyHandler(Object proxied){
    
    
        this.proxied = proxied;
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
    
    
        // 在转调具体目标对象之前,可以执行一些功能处理
        System.out.println("前置增强处理: yoyoyo...");
        // 转调具体目标对象的方法(三要素:实例对象 + 实例方法 + 实例方法的参数)
        Object obj = method.invoke(this.proxied, args);
        // 在转调具体目标对象之后,可以执行一些功能处理
        System.out.println("后置增强处理:hahaha...");
        return obj;
    }
}

(4)客户端

public class Test{
    
    
    public static void main(String[] args){
    
    
        Subject proxied = new RealSubject();//真实对象real
        /**
         * newProxyInstance有3个参数,分别是
         * ClassLoader loader:用哪个类加载器去加载代理对象
         * Class</?>[] interfaces:动态代理类需要实现的接口
         * InvocationHandler h:动态代理方法在执行时,会调用h里面的invoke方法去执行
         */
        Subject proxySubject = (Subject) Proxy.newProxyInstance(
                Subject.class.getClassLoader(),
                new Class[]{
    
    Subject.class},
                new ProxyHandler(proxied));
        proxySubject.doSomething();
        System.out.println("代理对象的类型 : " + proxySubject.getClass().getName());//输出:pack.$Proxy0
        System.out.println("代理对象所在类的父类型 : " + proxySubject.getClass().getGenericSuperclass());
        //输出class java.lang.reflect.Proxy
    }
}

示例

class TraceHandler implements InvocationHandler{
    
    
    private Object proxied;
    public TraceHandler(Object proxied){
    
    
        this.proxied = proxied;
    }
    @Override
    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable{
    
    
        //这里是增强信息:输出方法信息
        System.out.print(proxied);//输出proxied,这里因为是Integer,所以就是输出数字大小
        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(")");
        //这里是被代理类的原方法
        //调用实际的invoke执行方法m
        return m.invoke(proxied, args);
    }
}
public class ProxyTest {
    
    
    public static void main(String[] args){
    
    
        Object[] elements = new Object[10];
        for(int i=0; i<elements.length; ++i){
    
    
            //从这里开始进入代理流程
            //1. 生成一个被代理类的对象,这里的被代理类是Integer
            //Integer内部实现了接口Comparable
            Integer value = i+1;
            //2. 动态创建代理对象
            //这里一般由实现的接口类型去接newProxyInstance,但由于Object也是接口的超类,因此也可以用Object去接
            TraceHandler handler = new TraceHandler(value);
            Object proxy = Proxy.newProxyInstance(null, new Class[]{
    
    Comparable.class}, handler);
            //Comparable proxy = (Comparable) Proxy.newProxyInstance(Comparable.class.getClassLoader(), new Class[]{Comparable.class}, new TraceHandler(value));//等价于上面的语句
            elements[i] = proxy;//将各个代理存入到数组中
            //proxy.toString();//指定要执行的方法
            //System.out.println(proxy);//默认执行了toString方法,返回结果为value的String形式
        }
        Integer key = new Random().nextInt(elements.length)+1;//随机生成0到elements.length的整数
        int result = Arrays.binarySearch(elements, key);//二分查找;每次比较大小都需要经过代理调用compareTo()方法
        if(result >= 0) System.out.println(elements[result]);//执行该对象的代理
    }
}

结果分析
在这里插入图片描述
以随机key=10为例:
在binarySearch处通过代理调用了3次compareTo()方法
在println处通过代理调用了1次toString()方法
最终再输出一个返回值,即toString的返回结果“10”

猜你喜欢

转载自blog.csdn.net/widsoor/article/details/127837250