JAVA笔记八:接口、lambda表达式与内部类

一、接口

1、概念

接口(interface)用来描述类应该做什么,而不指定他们具体应该如何做。一个类可以实现一个或多个接口

public interface Comparable

{

        int compareTo(Object other);

}

任何实现Comparable接口的类都需要包含compareTo方法,这个方法有一个Object参数。

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

接口还可以定义常量,但绝不会有实例字段,实例字段和方法实现的任务应该由实现接口的那个类来完成。

因此接口可以看为没有实例字段的抽象类。 

2、实现

让类实现一个接口,通常需要两个步骤

1、使用关键字implements将类声明为给定的接口

2、对接口中的方法提供定义

 class Emloyee implements Comparable

public int compareTo(Object otherObject)

{

        Employee other = (Employee) otherObejct;

        return Double.compare(salary,other.salary);

在声明接口中,所有的方法自动为public, 不过在实现接口时,必须把方法声明为public。

为什么不能在Employee类中直接提供compareTo方法,而必须实现Comparbale接口呢?

    主要原因是Java是一种强类型语言,在调用方法时候编译器要能检查这个方法确实存在

        if(a[i].compareTo(a[j]>0))

        {

                //.......

        }

     编译器必须确认a[i]一定有一个compreTo方法,如果a是一个Comparable对象的数组,就       可以确保有compareTo方法,因为每个实现Comparable接口的类都必须提供这个方法的定义。

3、接口属性

接口不是类,不能使用new来实例化一个接口,但可以声明接口的变量。接口变量必须引用实现了这个接口的类对象

x = new Comparable  //错误

Comparable x             //正确

x = new Employee(....)

使用关键字instanceof检查一个对象是否实现某个特定的接口

if(anObject instanceof Comparable)

 接口不能包含实例字段,但可以包含常量。与接口中的方法都自动设置为public一样,接口中的字段总是public static final。

4、接口与抽象类

Java的每个类只能扩展一个类,不能扩展第二个类,接口可以提供多重继承,同时还能避免多重继承的复杂性和低效性。

允许在接口增加静态方法,但通常将静态方法放在伴随类。

5、默认方法

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

6、接口与回调

回调(callback)是一种常见的程序设计模式,在这种模式下,可以指定某个特定事件发生时应该采取的动作。

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Instant;

public class TimerTest {
    public static void main(String args[])
    {
        var listener = new TimePrinter();
        var timer = new Timer(500,listener);
        timer.start();
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }

}
class TimePrinter implements ActionListener
{
    public void actionPerformed(ActionEvent event)
    {
        System.out.println("时间为:"+ Instant.ofEpochMilli(event.getWhen()));
        Toolkit.getDefaultToolkit().beep();
    }

}

使用TimerPrinter接口类重写了ActionListener接口中的actionPerformed方法,输出事件与响铃,

将TimerPrinter的实例listenter对象传入Timer()构造器中。timer.start()开始执行,Timer()构造器第一个参数为时间间隔,第二个为监听器对象,即回调函数或对象。

使用 JOptionPane.showMessageDialog(null,"Quit program?");弹出对话框,等待点击,第一个参数与为null,对话框显示在屏幕中央。

javax.swing.Timer

  • Timer(int interval,ActionListener listener) 

//构造一个定时器,每经过interval毫秒执行listenter监听器一次

  • void start()   //启动定时器,一旦启动调用监听器的actionPerformed
  • void stop()   //停止定时器

 java.awt.Toolkit

  • static Toolkit getDuefaultToolkit()   //获得默认的工具箱
  • void beep()  //响铃

7、Comparator接口

如果我们假设希望按照长度增长的方法排序,要处理这种方法使用Arrays.sort(),这个方法还有第二种版本,有一个数组和一个比较器(comparator)

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

 要利用长度比较,可以定义一个实现Comparator<String>的类

class lengthComparable implements Comparable<String>
{
    public int compare(String first,String second)
    {
        return first.length()-second.length();

    }
}

 二、lambda表达式

1、概念

Lambda表达式,也可称为闭包,是JDK8的新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中),可以使代码变的更加简洁紧凑。Lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

lambda表达式本质是一个匿名函数

public int compare(String frist,String second)

{

        return frist.length()-second.length();

}

//可以转换为:

(String frist,String second)-> return frist.length()-second.length();

 语法格式如下:

()->结果表达式

(参数1,参数2,参数n)->结果表达式

()->{代码块}

(参数1,参数2,参数n)->{代码块}

既可以理解为

(parameters) -> expression

(parameters) ->{ statements; } 

1、 特别注意不能只在某些分支返回一个值,这是不合法的,如:

(int x)->{if(x>=0)return 1;}

2、即使lamdba表达式没有参数,仍然要提供空括号

()->{for(int i = 100;i>=0;i++) System.out.println(i);}

3、如果可以推导出参数类型,则可以忽略其类型: 

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

 2、函数式接口

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

定义一个函数式接口如下

@FunctionalInterface
interface GreetingService 
{
    void sayMessage(String message);
}

那么就可以使用Lambda表达式来表示该接口的一个实现

GreetingService greetService1 = message -> System.out.println("Hello " + message);

以Arrays.sort方法为例,它的第二个参数需要一个Compartor实例,Compartor就是只有一个方法的接口,所以可以提供一个lambda表达式

Arrays.sort(words,(first,second)->first.length()-second.length());

 函数式接口:

        前提:只有一个抽象方法的接口、

        结构:接口名 接口对象名 = lambda表达式

 实际上,在Java中,lambda表达式所能做的也只是转换为函数式接口。

3、lambda表达式实现无参、有参抽象方法

很多函数接口的抽象方法是无参数的,如线程接口Runnable接口只有一个run()方法,这样的无参抽象方法在lambda表达式中使用“()”表示。而对于有参方法,使用如下结构

(参数1,参数2,参数n)->{代码块} 

public class TimerTest {
    interface Add
    {
        abstract int add(int a,int b);
    }
    public static void main(String args[])
    {
        Add aaa = (x,y)->x+y;
        int result = aaa.add(1,2);
        System.out.println(result);
    }
}

4、lambda表达式可以更改类成员的变量,但无法更改final修饰的变量

public class TimerTest
{
    public static void main(String args[])
    {
        var xxxx = new Variable();
        xxxx.print();
        xxxx.action();
        xxxx.print();
    }

}
interface VariableInterface2
{
   void method();
}
class Variable
{
    private int value = 100;
    //利用lambda表达式修改常量值,函数式接口
    public void action()
    {
        VariableInterface2 v = ()->{
            value = -2;
        };
        v.method();
    }
    public void print()
    {
        System.out.println(value);
    }
}

5、lambda表达式与异常处理

很多接口的抽象方法为了保证程序的安全性,会在定义时抛出异常,但lambda表达式中并没有抛出异常的语法,这是因为lambda表达式会默认抛出抽象方法原有的已存异常,当此方法被调用时候则需要进行异常处理。

三、方法引用

方法引用可以认为是Lambda表达式的一种特殊形式,lamdba表达式为单方法即可转换为方法的引用。

 有时,lambda表达式涉及一个方法。例如假设你希望只要出现一个定时器时间就打印这个事件的对象。

可以调用:

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

也可以直接把println方法传递到Timer构造器中,如下:

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

 表达式System.out::println是一个方法的引用(method reference),它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法,在这个例子中,会生成一个ActionListener,.它的actionPerformed(ActionEvent e)方法要调用System.out.println(e)。

 注意:编译器需要根据上下确定使用哪一个方法,在我们的例子中,方法的引用用System.out::println必须转换为一个包含ActionListener实例(void actionPerformed(ActionEvent e))这样会从10个重载的println方法中选出println(Object x)方法,因为Object与ActionEvent最匹配。

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

        1、object::instanceMethod

        2、class::staticMethod

        3、class::instanceMethod

 例1、引用方法成员

对象名::方法名

package cn.aaa.bbbb.java;

public class Test {
    public static void main(String args[])
    {
        //对象名引用方法: MethodInterface B = A::add;
        var A = new Method();
        MethodInterface AA = A::add;
        int BB = AA.add(1,2);

        //lambda表达式,函数式接口:MethodInterface D = (a,b)->C.add(a,b);
        var C = new Method();
        MethodInterface CC = (a,b)->C.add(a,b);
        int DD = CC.add(1,2);

        //若方法为static,用类名引用  MethodInterface FF = MethodDemo::add;
        MethodInterface F = MethodDemo::sub_add;
        int FF = F.add(1,2);



    }
}
interface MethodInterface
{
    int add(int a,int b);
}
class Method
{
    public int add(int a,int b)
    {
        return a+b;
    }
}
class MethodDemo
{
    public static int sub_add(int a,int b)
    {
        return a+b;
    }
}

1,这种模式下, concatString()方法不能标记为静态方法,否则编译会报错。

2,这里的对象可以是父对象,比如可以使用:

        super:: concatString

例2、引用静态方法

类名::静态方法名

public class TimerTest {
    public static void main(String args[])
    {

        //方法的引用
        StaticMethodInterface methodInterface = StaticMethodDemo::add;
        int result = methodInterface.method(1,2);
        System.out.println(result);

    }

}
//创建函数式接口
interface StaticMethodInterface
{
    int method(int a,int b);
}
class StaticMethodDemo
{
    public static int add(int a,int b)
    {
        return a+b;
    }
}

 例3、类::实例方法

//

下表列出了方法引用的示例

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

例如 s->s.length()==0,这个有一个方法调用,但还有个比较,所以不能使用方法引用。

 四、构造器的引用

构造器引用与方法引用很类似,只不过方法名为new。假设person::new是person构造器的一个引用,选择哪个构造器这取决于上下文,假设有一个字符串列表,可以把他转换为person对象数组,所有要在各个字符串上调用构造器。

ArraysList<String> name = ...;

Stream<person>stream = names.stream().map(person::new);

List<person> people = stream.collect(Collect.toList());

map方法会为各个列表元素调用person(String),如果有多个person构造器,编译器从上下文推到出一个带有String参数的构造器。

lambda表达式有3种引用构造的语法,分别为引用无参构造器,引用有参构造器,引用数组构造器。

例1、引用无参构造器

类名::new

package cn.aaa.bbbb.java;

public class Constructors {
    public static void main(String args[])
    {
        ConstructorsInterface a = ConstructorsDemo::new;
        //a.action(2);
        a.action();//编译器根据上下文推断,从而调用无参构造器

    }
}
interface ConstructorsInterface
{
    ConstructorsDemo action();
}
class ConstructorsDemo
{
    public ConstructorsDemo()
    {
        System.out.println("调用无参构造器");
    }
    public ConstructorsDemo(int i)
    {
        System.out.println("调用有参构造器,参数为"+i);
    }
}

创建函数式接口, 测试类中有无参与有参两种构造器,接口抽象方法返回值为测试类名,并且无参。利用引用无参构造器创建接口对象,并且调用接口对象所创造的方法创建测试类对象,并查看输出结果。

例2、引用有参构造器

//代码同上,调用a.action(2);

 例3、引用数组构造器

/**/

五、变量作用域

一个lambda表达式包含:参数、一个代码块、自由变量的值(这里指非参数而且不在代码中定义的变量)

考虑如下例子:

 public static void repeatMessage(String str)
    {
        ActionListener listener = event->{
            System.out.println(str);
        };
    }

 调用如下

repeatMessage("Hello");

 其中lamdba表达式中变量test,该变量并不是在lambda表达式中定义的。实际上test为repeatMessage方法中的一个参数,故称test为lambda表达式的自由变量,表示lambda表达式的数据结构必须存储test自由变量的值,即为Hello。我们说它被lambda表达式所捕获(captured)。

可以看到lambda表达式可以捕获外围作用域中变量的值。但捕获是具有限制的

1、在lamdba表达式,只能引用值不会改变的变量

        例:

public static void countDown(int start)
{
    ActionListener listener = event->{
        start--;
        //ERROR,不能引用改变的变量
        System.out.println(start);
    };
}

2、在lamdba表达式中引用一个变量,而这个变量可能在外部改变

        例:

public static void repeat(String str)
{
for(int i = 0;i<10;i++)
    ActionListener listener = event->{
        System.out.println(i+str);
        //ERROR,i值在外部已经改变
    };
} 

综上:

1、lambda表达式中捕获的变量必须为事实最终变量。事实最终变量是指,这个变量在初始化后就不会再为它赋新值。

2、lambda表达式的体与嵌套块有相同的作用域,在lambda表达式声明一个局部变量同名的参数或局部变量是不合法的。

六、处理lambda表达式、Fuction接口

使用lambda表达式的重点是延迟执行(deferred)

使用lambda表达式都需要创建或调用已有的函数式接口,java.util.function包提供了很多预定义函数式接口,就是没有实现任何功能,仅用来封装lambda表达式对象。

该包常用接口是Function<T,R>接口,Function接口是函数式接口,所有有一个抽象方法。

T:被操作的类型,可理解为方法参数类型

R:操作结果类型,可以理解为方法的返回类型

 Function接口提供了3个已实现的方法。如下表

 常用的函数式接口

 

例1、使用lambda表达式拼接ip地址

package cn.aaa.bbbb.java;
import java.util.function.Function;
public class ConnectIp
{
    public static void main(String args[])
    {
        Integer[] ip = {192,168,1,1};
        FunctionDemo functionDemo = new FunctionDemo();
        System.out.println(functionDemo.function.apply(ip));

    }
}
class FunctionDemo
{
    //方法参数为Integer数组
    Function<Integer[], String> function = (ip)->{
        //创建字符序列
        StringBuilder str = new StringBuilder();
        for (Integer num:ip)
        {
            //添加数组元素,并以.分割,形如num.num.num.num.
            str.append(num);
            str.append('.');
        }
        str.deleteCharAt(str.length()-1);//删除最后追加的.
        //以字符串返回到参数ip中
        return str.toString();
    };
}

 

例2、将一个动作重复n次

1、使用Runnable函数式接口,利用lambda表达式

public class hello_world{
    public static void main(String[] args) {
        repeat(10,()->System.out.println("hello"));
    }
    public static void repeat(int n,Runnable action)
    {
        for (int i = 0;i<n;i++)
            action.run();
    }
}

当调用action.run()时会执行lambda表达式的主体

2、利用方法的引用实现函数式接口

public class hello_world{
    public static void main(String[] args) {
        //repeat(10,()->System.out.println("hello"));
        Runnable PP = Print::printf;
        repeat(10,PP);
    }
    public static void repeat(int n,Runnable action)
    {
        for (int i = 0;i<n;i++)
            action.run();
    }
}
class Print
{
    public static void printf()
    {
        System.out.println("hello world");
    }
}

定义测试类Print,创建接口对象PP,PP引用测试类中的printf方法, 再通过repeat实现

例3、利用IntConsumer函数式接口,显示动作出现在哪一次迭代

public class Math_{
    public static void main(String[] args)
    {
        //利用lambda表达式实现函数式接口
        repeat(10,i->System.out.println("Countdown:"+(9-i)));
    }

    public static void repeat(int n,IntConsumer action)
    {
        for (int i = 0;i<n;i++)
            action.accept(i);
    }
}
interface IntConsumer
{
    void accept(int value);
}

猜你喜欢

转载自blog.csdn.net/m0_61598337/article/details/128511343