07-接口

接口

  • 接口中所有方法都是public
  • 接口允许多重继承
  • 实现接口必须声明为public

Comparable接口

  • Arrays.sort(Comparable obj)方法要求一个实现了Comparable接口的对象
  • 因为这个排序会调用Comparable的comparaTo方法
  • 通过这个comparaTo方法可以自定义排序规则

comparaTo的实现规则:

  • this<other,返回负整数
  • this=other,返回0
  • this>other,返回正整数

比如,如果想要按照员工的薪水进行排序

//首先,这个类要实现Comparable接口
public class Employee implements Comparable{
    ...;
    
    //实现comparaTo方法,则会按照compareTo方法定义的规则排序
    @override
    public int comparaTo(Object obj){
        Employee other = (Employee) obj;
        return Double.compare(salary, obj.salary);
    }
}

更优的方法,实现泛型接口

//首先,这个类要实现Comparable接口
public class Employee implements Comparable<Employee>{
    ...;
    
    //实现comparaTo方法,则会按照compareTo方法定义的规则排序
    @override
    public int comparaTo(Employee em){
        return Double.compare(salary, em.salary);
    }
}

Comparator接口

Arrays.sort()有一个重载方法,可以接受一个Comparator

Comparator使用场景:有的类没有继承Comparable接口,并且也不能再继承这个接口,可以传入一个Comparator来定义排序规则

//自定义按字符串长度排序
public LengthComparator implements Comparator<String>{
    @override
    public int compare(String first, String second){
        return first.length-sencond.length;
    }
}

//使用这个比较器
String[] fields = {"asdh","asda","asdasdf"};
Arrays.sort(fields, new LengthComparator);

Cloneable接口

  • clone()方法是Object类的一个protected方法
  • 直接调用这个clone()方法得到的是一个浅拷贝(对象引用是一样的,并没构造新对象
  • Cloneable是一个标记接口,表示这个类可以调用copy()方法

创建深拷贝:

class Employee implements Cloneable{
    @override
    public Employee clone() throws CloneNotSupportedException{
        //调用Object.clone()得到一个浅拷贝
        Employee cloned = (Employee) super.clone();
        //对于引用对象,再次调用这个对象自己的clone()方法
        cloned.hireDay = (hireDay) cloned.hireDay.clone();
        
        return cloned;
    }
}

注意到CloneNotSupportedException这个异常,这种深拷贝有很多限制

其中的引用对象也必须是实现了Cloneable接口的类,否则则会抛出异常,拷贝失败

数组有一个clone方法,可以建立一个深拷贝的副本

int[] a={1,2,3,4,5};
int[] cloned = a.clone();
cloned[0]=100;//a数组中的值不会变

接口的属性

  • 接口中不能包含实例字段,但可以包含常量

  • 接口中的常量总是public static final

  • 接口不能实例化

  • 接口变量必须引用实现了这个接口的类对象(多态),可以使用instanceof检验是否实现了某个接口

接口的默认实现

使用default为接口方法提供默认实现

默认方法可以事先调用还没实现的抽象方法

//默认方法
public interface Comparable<T>{
    default int compareTo(T other){return 0;}
}

//默认方法调用抽象方法
public interface Collection{
    //抽象方法
    int size();
    default boolean isEmpty{
        return this.size() == 0;
    }
}

方法冲突

  1. 超类优先:超类中有同名和相同参数类型的方法优先

  2. 接口冲突:多个接口中有同名同参数的默认方法,需要自行解决

    • 多个接口中的同名同参数方法都会被继承,如果需要调用到超类的方法,必须要显式给出
    • 只要有一个接口的同名同参数方法是默认方法,就需要显式继承
    • 非默认方法,直接@override完事
    interface Person{
        default String getName(){return "1"};
    }
    interface Named{
        default String getName(){return "2"};
    }
    
    //如果要调用到父类的该方法,应显式给出
    class Student implements Person,Named{
        @override
        public String getName(){
            return Person.super.getName();
        }
    }
    

回调

一种常见的程序设计模式,指等待某个事件发生后采取的动作

面向过程的语言一般采用函数传递的方法

但java一般是把某方法封装在接口内,然后要求一个实现了该接口的对象,将这个对象传递

比如,java.swing.Timer希望一定事件间隔得到通知,一定时间间隔后触发通知函数,即回调

这个Timer的构造方法则需要接收一个函数式的对象(这个对象实现了某种接口,重写了其中的方法,Timer会调用这个方法

//实现ActionListener的actionPerformed()方法
public TimerPrinter implements ActionListener{
    @override
    public void actionPerformed(ActionEvent event){
        System.out.println("time is"+Instant.ofEpochMilli(event.getWhen()));
        Toolkit.getDefaultToolkit().beep();
    }
}

//传给Timer进行调用
Timer t = new Timer(1000, new TimerPrinter);
t.start;

lambda表达式

参考上面回调的例子,我们仅仅是想要传入某个方法进去,但是却不得不实现一个接口然后传入他的对象

这样其实不太方便,lambda表达式实际上就是一段代码块,但是本质却可以生成一个实现接口的对象传传递到需要的地方

lambda表达式的语法

(Param1,Param2,...) ->{expressions;}

  • 参数如果可以从代码块中推断出类型,则前面可以不给出类型(最好还是给出来
  • 即使不传参数,也要写()
  • 无需指定返回值,lambda表达式会根据上下文进行推导
  • 写在代码块中则需要指定返回值
//使用lambda表达式实现一个Comparator
Comparator<String> com = (String first,String second) -> first.length()-second.length();
Comparator<String> com = (String first,String second) -> {return first.length()-second.length();};

函数式接口

所谓函数式接口,就是只有一个抽象方法的接口,唯一的用处就是用来传递方法

这种接口使用lambda表达式非常便捷

仅能把lambda表达式赋给函数式接口

//自定义实现ArrayList中的removeIf()
list.removeIf(e -> e==null);

//原本的函数式接口
public interface Predicate<T>{
    boolean test(T t);
}

方法引用

使用场景不多,不好用

仅在lambda表达式只调用一个方法时才能使用

使用::分隔方法和对象名/类名

  • object::intanceMethod
  • Class::instanceMethod
  • Class::staticMethod
//以下几种表达式等价
Timer timer = new Timer(1000, event -> System.out.println(event));
Timer timer = new Timer(1000, System.out::println);

String::trim
x -> x.trim

使用thissuper也是合法的

比如,this::equals等价于x -> this.equals(x)

构造器引用

  • Class::new

比如new Person()等同与Person::new

具体调用哪个构造器由上下文决定

变量作用域

  • lambda表达式可以捕获外部变量的值,但是只能引用值不会改变的变量(事实最终变量

    //这样是可以的,text不会被修改
    public void repeat(String text){
        new Timer(1000, event -> {
            System.out.println(text);	//text被捕获
        }).start();
    }
    
    //捕获变量后在lambda表达式修改是非法的
    public void repeat(String text, int i){
        new Timer(1000, event -> {
            System.out.println(text);
            System.out.println(i);
            i--;	//非法,不允许修改
        }).start();
    }
    
    //捕获在外部会修改的变量也是非法的
    public void repeat(String text){
    	for(int i=10; i>0; i--){
            new Timer(1000, event -> {
                System.out.println(text);
                System.out.println(i);	//非法,捕获了外部会修改的值
            }).start();
        }
    }
    
  • 参数与局部变量同名也是非法的

    Path first = Path.of("/usr/bin");
    Comparator<String> comp = (first, second) -> first.length()-second.length();	//非法,first重名了
    
  • lambda表达式子中的this指的是类对象

    class test{
        public void repeat(String text){
        	//this指的是test对象引用而不是ActionListener
        	ActionListener listener = event -> {System.out.println(this.toString);}
        	new Timer(1000, listener).start();
    	}
    }
    

处理lambda表达式

在需要用到函数式接口的地方使用

使用lambda表达式的重点是延迟执行,将这个方法传递进去,具体什么时候执行不知道

使用@FunctionalInterface来标注一个函数式接口

比如,要写一个repeat方法重复执行某些操作

public static void repeat(int n, Runnable ation){
    for(int i=0; i<n;i++){
        action.start();
    }
}

//可以这样使用repeat
repeat(10, () -> System.out.println("balabla"));

Comparator进阶

07-接口

comparing方法返回的还是一个Comparator对象

但是comparing方法接收的是一个比较函数的接口(Function

比如,要按照人名进行排序

//返回一个按照人名排序的比较器
Arrays.sort(people, Comparator.comparing(Person::getName));

相应的对于int类型的数据有comparaInt()方法,对于double类型的数据compareDouble()等

还可以使用thenCompare()指定第二条件、第三条件...

//优先按照名字排序,名字相同按照年龄排序
Arrays.sort(people, Comparator.comparing(Person::getName).thenCompareInt(Person::getAge));

内部类

  • 仅内部类所属的外部类可见
  • 内部类可以访问其外部类的数据,包括private数据

使用内部类访问外部字段

观察下面的例子,只要不调用start()方法,则内部类不会被实例化

使用内部类直接就能访问类的私有字段,很好理解,因为private就是仅本类可见的意思,而内部类恰好在这个类中

底层:内部类在实例化时,就会默认构造一个指向外部对象的引用,Class.this指的就是外围类的对象

public class TimeClock {
    private boolean isBeep;

    public TimeClock(boolean isBeep) {
        this.isBeep = isBeep;
    }

    public void start(){
        ActionListener listener = new InternalTimerPrinter();
        Timer t = new Timer(1000, listener);
        t.start();
    }

    public class InternalTimerPrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("time is:"+e.getWhen());
            if(isBeep)	//可以写成TimeClock.this.isBeep
            {	
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
}
  • 设置内部类为private

    仅内部类可以设置为private,此时只能被该类的内部访问和实例化

  • 设置内部类为public

    外面的其他类或者其他对象也可以访问这个内部类

    比如,如果想把内部类的对象那出来到外部

    TimeClock tc = new TimeClock(false);
    //拿到内部类对象
    TimeClock.InternalTimerPrinter listener = tc.new InternalTimerPrinter();
    

内部类的属性

  • 索引静态字段必须是final,且必须初始化

  • 不能有static方法

  • 内部类是一个编译器现象,对于JVM来说看不出来他是一个内部类

    编译时,会自动在内部类添加对外围类的引用和获取外围类字段的方法

    但是可能不安全

局部内部类

如果仅仅在某个地方只使用一个对象一次,则可以局部定义这个类

比如,上面的例子中InternalTimerPrinter只在start方法中出现,可以改写为:

//将内部类写在了方法内部而已
public void start(){
    class InternalTimerPrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("time is:"+e.getWhen());
            if(isBeep)	//可以写成TimeClock.this.isBeep
            {	
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }           
    ActionListener listener = new InternalTimerPrinter();
    Timer t = new Timer(1000, listener);
    t.start();
}
  • 注意:局部变量不能有访问修饰符,作用域为这个块中,对块以外完全隐藏

局部内部类可以访问局部变量(块作用域,但是和lambda一样只能访问不可变的变量

public void start(boolean isBeep){
    class InternalTimerPrinter implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("time is:"+e.getWhen());
            if(isBeep)	//isBeep为局部变量isBeep,由start方法传入
            {	
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }           
    ActionListener listener = new InternalTimerPrinter();
    Timer t = new Timer(1000, listener);
    t.start();
}

匿名内部类

场景:只想使用某个类的对象,暂时用一下然后其他地方不用了,懒得单独去定义,甚至名字都不需要

//语法
new SuperType(parameters[]){
    data and methods of inner class...
}

也就是说,内部类是一个继承类,是实现了某个接口或者类的类

  • 匿名内部类也是内部类,所以具有访问外围类字段的作用
  • 匿名内部类在方法内部,那也一样具备局部内部类的性质
  • 匿名内部类没有类名,也就没有构造器
public void start(boolean isBeep){         
    ActionListener listener = new ActionListener(){
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("time is:"+e.getWhen());
            if(isBeep)
            {	
                Toolkit.getDefaultToolkit().beep();
            }
        }
    };	//这是一个实现了ActionListener的类
    Timer t = new Timer(1000, listener);
    t.start();
}

静态内部类

场景:只是为了隐藏某个类,不需要对外围类有引用,则可以定义为static

static方法中调用内部类对象,则内部类必须是static

上一篇:Node.js


下一篇:v-on