接口、lambda表达式及内部类

接口

        接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的同一格式进行定义。接口中所有方法自动的属于public。因此,在接口中声明方法时,不必提供关键字public

        举个例子:Array类中的sort方法可以对对象数组进行排序,但必须满足对象所属的类必须实现了Comparable接口。(在JavaSE 5.0中,Comparable接口已经改进为了泛型类型)

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

        这就是说,任何实现Comparable接口的类都必须要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。

        接口绝对不能含有实例域。提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此可以将接口看成是没有实例域的抽象类。为了让类实现一个接口,需要:

  • 将类声明为实现给定的接口
  • 对接口中的所有方法进行定义
  • 将类声明为实现某个接口,需要使用关键字implements

接口的特性

  1. 接口不是类,尤其是不能使用new运算符实例化一个接口。不能构造接口的对象,但是可以声明接口的变量;接口变量必须引用实现了接口的类对象。
  2. 如同使用instandceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口。
  3. 与可以建立类的继承关系一样,接口也可以被扩展。
  4. 接口中不能包含实例域或静态方法,但是可以包含常量

        Java中有一个很重要的内置接口,称为Cloneable。如果某个类实现了这个接口,Object类中的clone方法就可以创建类对象的一个拷贝。如果希望自己设计的接口具有克隆和比较的能力,只需要实现这两个接口即可。

class Human implements Cloneable,Comparable

接口和抽象类

        抽象类表示通用属性存在这样的问题:每个类只能扩展于一个类。假设Human类已经扩展于一个类了,它就不能再这样扩展第二个类了

class Superman extends Human,Comparable//Error

        但是每个类都可以实现多个接口 

class Superman extends Human implements Comparable

        Java不支持多重继承,其主要原因是多继承会让语言变得非常复杂,效率也会降低。实际上,接口可以提供多继承的大多数好处,同时还可以避免多继承的复杂性和低效性

默认方法

        可以为接口方法提供一个默认实现。必须使用default修饰符标记这样一个方法。有些情况下,默认方法很管用。例如,如果希望在发生鼠标点击事件时得到通知,就要实现下面这个接口:

public interface MouseListener
{
    void mousedieked(MouseEvent event);
    void mousePressed(MouseEvent event);
    void mouseReleased(MouseEvent event);
    void mouseEntered(MouseEvent event);
    void mouseExited(MouseEvent event);
}

        大多数情况下,只需要关心其中1、2个事件类型。在Java SE 8中,可以把所有的方法声明为默认方法,这些默认方法什么也不做

public interface MouseListener
{
    default void mousedieked(MouseEvent event) {}
    default void mousePressed(MouseEvent event) {}
    default void mouseReleased(MouseEvent event) {}
    default void mouseEntered(MouseEvent event) {}
    default void mouseExited(MouseEvent event) {}
}

        这样,实现这个接口只需要为真正关心的事件覆盖相应的监听器。默认方法可以调用任何其他方法。

解决默认方法冲突

        如果先在一个接口中将一个方法定义成了默认方法,然后又在父类或者另一个接口中定义了同样的方法。那么它们将遵循下面的规则:

  1. 父类优先。如果父类提供了一个具体方法,同名且有相同参数类型的默认方法会被忽略
  2. 接口冲突。如果一个父接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法来解决冲突

        假如有一个类同时实现了Person和Named接口。这两个接口都有getName方法。但是并不是从这两个接口中选择一个来实现,java编译器会报告这个错误,让程序员来解决这个二义性。这个时候就需要这个类提供一个getName方法了。在这个方法中可以选择两个冲突方法中的一个

        假如一个类继承了父类,同时拓展了接口,并从接口和父类中继承了相同的方法。在这种情况下,只会考虑父类方法,接口的所有默认方法都会被忽略。这就是"类优先"规则

接口示例

接口与回调

        回调是一种常见的设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。例如,鼠标在选择某个菜单项的时候应该采取什么行动。

        举个例子:在java.swing包中有一个Timer类,可以使用它在到达给定时间间隔时发出通告。

public class TimeClock {
    public static void main(String[] args) {
        ActionListener listener = new TimePrinter();
        //每隔10秒打印一条信息
        Timer timer = new Timer(10000,listener);
        //启动计时器
        timer.start();
        JOptionPane.showMessageDialog(null,"Quit program?");
        System.exit(0);
    }
}
class TimePrinter implements ActionListener{
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("At the tone,the time is "+new Date());
        Toolkit.getDefaultToolkit().beep();
    }
}

Comparator接口

        在前面已经有将对象数组按照字典顺序排序的方法了;如果现在希望按长度递增的顺序来进行排序,首先肯定不能让String类用两种不同的方式实现compareTo方法,另外,String类我们也不能修改。

        要处理这种情况,Arrays.sort还有第二个版本,有一个数组和一个比较器作为参数

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

要按数组长度比较字符串,可以像下面这样定义一个实现Comparator<String>的类

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

具体完成比较的时候则需要建立一个实例

Comparator<String> comp = new LengthComparator();
if (conp.compare(words[i], words[j]) > 0){
}

对象克隆

        Cloneable接口,这个接口指示一个类提供了一个安全的clone方法。如果原变量和副本都是同一个对象引用的,这说明,任何一个变量改变都会影响另外一个变量。如果希望copy是一个新对象,它的初始状态与original相同,但是它们之后会有各自不同的状态,这种情况下就应该使用clone方法。

         clone方法是Object的一个protected方法,这说明不可以直接调用这个方法,只有相同的类的对象才可以进行clone。如果对象包含子对象的引用,拷贝域就会获取相同子对象的另一个引用,这样,原对象和克隆的对象仍然会共享一些信息。

        默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。不过通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象。对于每一个类:

  1. 默认的clone方法是否都满足
  2. 是否可以在可变的子对象上调用clone来修补默认的clone方法
  3. 是否不该使用clone

实际上第三个是默认选项,如果选第一项或者第二项,类必须:

  1. 实现Cloneable接口
  2. 重新定义clone方法,并指定public访问修饰符 

Lambda表达式 

        lambda表达式是一个可传递的代码块,可以在以后传递1次或多次。

        JAVA中有很多封装代码块的接口,对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口

        有时,可能已经有现成的方法可以完成你想传递到其他代码的某些动作。例如,假设你希望只要出现一个定时器事件就打印这个事件对象。

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

        但是,如果直接把println方法传递到Timer构造器就更好了

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

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

        要使用 ::操作符分隔方法名与对象或类名。主要是一下三种情况:

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

        在前两种情况中,方法引用等价于提供方法参数的lambda表达式。对于第三种情况,第一个参数会成为方法的目标。例如String::compareToIgnoreCase 等价与(x, y) -> x.compareToIgnoreCase(y)

构造器引用

        构造器引用与方法引用很类似,只不过方法名为new。例如:Person::new是Person构造器的一个引用。具体是哪个构造器取决于上下文。

        可以用数组类型建立构造器引用。但JAVA有一个限制,无法构造泛型类型T的数组。而数组构造器的引用对于克服这个限制很有用。假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:Object[] people = stream.toArray(); 

        不过,用户希望得到的是一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[] :: new 传入toArray方法

Person[] people = stream.toArray(Person[] :: new);

变量作用域

        我们可能会希望能在lambda表达式中访问外围方法或类中的变量。先来看一下lambda表达式的组成部分

  1. 代码块
  2. 参数
  3. 自由变量的值,这里指非参数而且不在代码中定义的变量
public static void repeatMessage(String text,int delay){
        ActionListener listener = event -> {
            System.out.println(text);
            Toolkit.getDefaultToolkit().beep();
        };
        new Timer(delay,listener).start();
    }
repeatMessage("Hello",1000);

        在上面的例子中,这个lambda表达式有一个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串“Hello”。它被lambda表达式捕获。因此lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的。在lambda表达式中只会引用值不会发生改变的变量。

        之所以会这样限制的原因就是如果在lambda表达式中改变变量,并发执行多个动作的时候就会不安全。如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的

处理lambda表达式

        使用lambda表达式的重点是延迟执行。之所以希望以后再执行代码:

  • 在一个单独的线程中运行代码
  • 多次运行代码
  • 在算法的适当位置运行代码
  • 发生某种情况时执行代码
  • 只在必要时执行代码

内部类

        内部类是定义在另一个类中的类。使用内部类的主要原因有以下三点

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

使用内部类访问对象状态 

        内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域

package StudyInterface;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Date;

/**
 * @author wangzhixiang
 */
public class InnerClassTest {
    public static void main(String[] args) {
        TalkingClock 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(){
        ActionListener listener =new TimePrinter();
        Timer timer = new Timer(interval,listener);
        timer.start();
    }
    public class TimePrinter implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("the time is"+new Date());
            if(beep){
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
}

TimePrinter类声明为私有的。这样,只有TalkingClock的方法才能够构造TimePrinter对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公开可见性

内部类的特殊语法规则

        内部类中声明的所有静态域都必须是final。我们希望一个静态域只有一个实例,不过对于每个外部对象,会分别有一个单独的内部类实例。如果这个域不是final,那他就可能不是唯一的。

        内部类不能有static方法。也可以允许有静态方法,但只能访问外围类的静态域和方法

        在上面的代码中,可以发现,TimePrinter这个类的名字只在start方法中创建这个类型的时候使用了一次。当遇到这种情况的时候,可以在一个方法中定义局部内部类。

public void start(){
    class TimePrinter implements ActionListener{
        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("the time is"+new Date());
            if(beep){
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    ActionListener listener =new TalkingClock.TimePrinter();
    Timer timer = new Timer(interval,listener);
    timer.start();
}

        局部类不能使用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类有一个优势,即对外部可以完全隐藏起来。即使TalkingClock类中的其他代码也不能对它进行访问。即除了start方法外,没有任何方法知道TimePrinter类的存在

        与其他内部类相比较,局部类还有一个优点。它们不仅可以访问包含它们的外部类,还可以访问局部变量。不过,局部变量必须事实上为final

        假如只创建这个类的一个对象,就不需要命名了、这种类被称为匿名内部类

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接口的类的新对象,需要实现的方法actionPerformed定义在{}中

        由于构造器的名字必须与类名相同,而匿名类没有类名,所以匿名类也不能有构造器。取而代之的是,将构造器参数传递给父类构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。如果构造参数的闭小括号后面跟了一个大括号,正在定义的就是匿名内部类。

上面的start方法用lambda表达式来写会简洁一些:

public void start(int interval, boolean beep) {
    Timer t = new Timer(interval, event -> {
        System.out.println("At the tone, the time is " + new Date());
        if (beep){
            Toolkit.getDefaultToolkit().beep();
        }
    });
    t.start();
}
使用内部类只是为了把一个类隐藏在另一个类内部,并不需要内部类引用外围类对象。因此,可以把内部类声明为static,以便取消产生的引用

下面是一个静态内部类的经典例子。计算数组中最小值和最大值的问题。 

package StudyInterface;

/**
 * @author wangzhixiang
 */
public class StaticInnerClass {
    public static void main(String[] args) {
        double[] d = new double[20];
        for (int i = 0; i < d.length; i++) {
            d[i] = 100 * Math.random();
        }
        ArrayAlg.Pair p  = ArrayAlg.minmax(d);
        System.out.println("min="+p.getFirst());
        System.out.println("max="+p.getSecond());
    }
}
class ArrayAlg{
    public static class Pair{
        private double first;
        private double second;
        public Pair(double f,double s){
            first = f;
            second = s;
        }
        public double getFirst(){
            return first;
        }

        public double getSecond() {
            return second;
        }
    }
    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);
    }
}

参考文献 : 《JAVA核心技术 卷一》

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值