lambda表达式的前世今生

218 阅读6分钟

前言

本文分为以下几节

  • 内部类和局部类
  • 匿名内部类
  • lambda表达式

在Java程序语言中,内部类、局部类、匿名内部类统称为嵌套类。何为嵌套类?--在类里面定义的类称为嵌套类。

内部类

内部类和局部类的划分是根据类定义的位置决定的,参见下面的例子:

public class MyArrayList {

    private final static int SIZE = 10;
    private int[] arrayOfInts = new int[SIZE];

    public MyArrayList() {
        for (int i = 0; i < SIZE; i++) {
            arrayOfInts[i] = i;
        }
    }
    public Iterator iterator(){
        return new MyIterator();
    }

    public  class MyIterator implements java.util.Iterator<Integer> {
        private int nextIndex = 0;

        public boolean hasNext() {
            return (nextIndex <= SIZE - 1);
        }

        public Integer next() {
            Integer retValue = Integer.valueOf(arrayOfInts[nextIndex]);
            nextIndex += 1;
            return retValue;
        }
    }

    public static void main(String s[]) {
        MyArrayList list = new MyArrayList();
        Iterator it = list.iterator();
        Iterator _it = list.new MyIterator();
        while (it.hasNext()){
            System.out.print(it.next()+" ");
        }
        System.out.println();
        while (_it.hasNext()){
            System.out.print(_it.next()+" ");
        }
    }
}

程序输出:
0 1 2 3 4 5 6 7 8 9
0 1 2 3 4 5 6 7 8 9 

在这个例子中我们定义了一个MyArrayList类,称为外部类。同时在这个类的Body中定义了一个MyIterator 类。MyIterator被称为内部类。
内部类具有一个特性,它可以很方便的访问外部类的字段和方法。并且对于外部世界来说具有更好的封装性。

为何我们在使用内部类的时候要依赖外部类呢?例如
MyArrayList list = new MyArrayList();
Iterator _it = list.new MyIterator();

原因很简单,看下面的图,虽然MyIterator是内部类,但是编译后便变成了一个单独的class文件。由于内部类中可能会操作到外部类的字段和方法,所以我们在new 内部类的时候必须要保证存在一个外部类的实例,并把这个实例隐式的传递给内部类(通过上面的语句编译器可以帮助我们完成传递)。

内部类也可以用static修饰,如果用static修饰那么它将只能访问外部类的static字段和方法,你可以通过Iterator _it =new MyArrayList.MyIterator();来实例化静态内部类。也可以通过MyArrayList.MyIterator.method()来直接调用静态内部类的静态方法。

局部类

再看一个例子:

public class HelloWorld {

    static String regularExpression = "[^0-9]";

    public static void validatePhoneNumber(
            String phoneNumber1, String phoneNumber2) {
        final int numberLength = 10;
        //局部类 start
        class PhoneNumber {
            String formattedPhoneNumber;
            PhoneNumber(String phoneNumber) {
                // numberLength = 7;
                // phoneNumber1 = "aaa";
                String currentNumber = phoneNumber.replaceAll(
                        regularExpression, "");
                if (currentNumber.length() == numberLength)
                    formattedPhoneNumber = currentNumber;
                else
                    formattedPhoneNumber = null;
            }

            public String getNumber() {
                return formattedPhoneNumber;
            }

            public void printOriginalNumbers() {
                System.out.println("Original numbers are " + phoneNumber1 +
                        " and " + phoneNumber2);
            }
        }
        //局部类 end
        PhoneNumber myNumber1 = new PhoneNumber(phoneNumber1);
        PhoneNumber myNumber2 = new PhoneNumber(phoneNumber2);
        
        if (myNumber1.getNumber() == null)
            System.out.println("First number is invalid");
        else
            System.out.println("First number is " + myNumber1.getNumber());
        if (myNumber2.getNumber() == null)
            System.out.println("Second number is invalid");
        else
            System.out.println("Second number is " + myNumber2.getNumber());
        //输出入参
        myNumber1.printOriginalNumbers();

    }

    public static void main(String... args) {
        validatePhoneNumber("123-456-7890", "456-7890");
    }
}

这个例子中在validatePhoneNumber()方法中定义了一个类PhoneNumber ,并依靠这个类对传入的参数(号码)进行了操作。我们将PhoneNumber 称为局部类。
可以看到局部类是定义在方法里的(准确的说是定义在块里),它不仅仅可以访问外部类字段,还可以访问方法的参数以及局部变量。

注意

  • 局部类只能访问参数和局部变量,并不能为其赋值!!!!也就是说如果有语句
    // numberLength = 7;
    // phoneNumber1 = "aaa";
    这样的语句编译器是会报错的
  • 局部类不能和内部类一样用static修饰,它是能否访问实例变量或静态变量完全由方法是否用static修饰决定。

为何不能为局部变量和参数赋值?

一开始我也百思不得其解,最后终于找到了答案。看下面的图,局部类同样也被编译成了两个单独的class文件。通常情况下我们认为让局部类访问局部变量和参数也没有什么问题,但是考虑一种特殊但是真实存在的情况,如果我们的局部类是个实现了Runable接口的类会怎么样呢?在方法里调用了内部类的run方法,有可能我们的方法执行完了线程类还在运行。这时候方法的栈帧已经销毁了,线程类无法操作局部变量和参数。所以局部变量和参数可以认为是复制了一份,传入局部类,局部类无法改变栈帧内的任何数据

匿名内部类

上面已经介绍了内部类和局部类,个人认为它们是具有很多共性的,仅仅是定义的位置有些不同(一个定义在外部类的Body中,一个定义在外部类的块里)。而匿名内部类和它们是有着本质区别的,匿名内部类是存在于表达式中的,与见下面的例子:

  public class HelloWorld {
    public static void main(String[] args) {
        Button btn = new Button();
        btn.setOnAction(new Listener() {
            @Override
            public void onClick() {
                System.out.println("Hello World!");
            }
        });
    }
}

上面的例子只是为了简单的阐述匿名内部类的使用场景,我们在btn.setOnAction()中new 了一个匿名内部类。从语句最后的;后就可以看出,它是存在于表达式中的。Listener是接口名,类的定义和实例化在同一瞬间完成。与Class A extend Listener这种类定义方式有着本质的不同。局部类和内部类仅仅是在定义类的位置上有所不同,而匿名内部类却连定义类的方式都焕然一新。

匿名内部类可以使我们的代码更加简短,并且减少了类的数量。广泛应用于类似按钮的回调函数中。因为每个按钮的回调内容都不相同,不可能为每个按钮的回调都定义一个类,这会使得类库十分庞大。所以匿名内部类就成功的解决了这一问题。

匿名内部类根据它存在的表达式的位置不同,也可以使用外部类的字段和方法中的局部变量。使用的规则和上述的内部类与局部类一致。

lambda表达式

尽管匿名内部类极大大简化了代码,但是还有升级的余地。难免有些人会觉得:我连个new字都不想写。见下面的例子:

  public class HelloWorld {
    public static void main(String[] args) {
        Button btn = new Button();
        btn.setOnAction(()->System.out.println("hello world"));
    }
}

上面的例子是lambda表达式的实际运用,它比匿名内部类更简洁。适用于接口内只有一个方法的情形。

lambda表达式由3个部分组成:
参数列表:()
箭头:->
方法体:{}

()中包含形参,->是一个固定存在的标志符,{}中为语句,当{}中只有一条语句时{}可省略,效果等同于

{   
   return System.out.println("hello world");   
}

参数的类型由编译时自动推断