内部类、lambda表达式、函数式接口、方法引用
一、内部类
1.1 概念
内部类是定义在另一个类中的类。
例:
public class OuterClass {
private String name = "外部类";
/*
内部类访问特点:
1. 内部类可以直接访问外部类的成员,包括私有
2. 外部类要访问内部类的成员,必须创建对象
*/
public class InnerClass {
public void display() {
System.out.println("name:" + name);
}
}
public void show() {
InnerClass i = new InnerClass();
i.display();
}
}
1.2 为什么要用内部类
- 内部类可以对同一个包中的其他类隐藏;
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
1.3 分类
- 成员内部类
- 局部内部类
- 匿名内部类(重要)
- 静态内部类
成员内部类,在类的成员位置定义,与其他成员变量一样,作为类的一个属性,可以有访问修饰符。
局部内部类,在类的局部位置定义(方法或代码块),声明局部内部类时不能有访问修饰符,即(public或private)。局部内部类的作用域被限定在声明这个局部类的块中。
1.4 详解
(1)成员内部类
同包下其他类如何访问一个类的内部类,需要通过外部类对象来创建内部类对象。
public class OuterTest {
public static void main(String[] args) {
// InnerClass i = new InnerClass(); //不能直接创建
OuterClass o = new OuterClass();
OuterClass.InnerClass oi = o.new InnerClass(); //通过外部类对象去创建
oi.display();
}
}
输出:
name:外部类
(2)局部内部类
例:在一个类的方法内创建内部类
public class OuterClass2 {
private int num = 10;
public void method() {
int num2 = 20;
class InnerClass2 {
public void show() {
System.out.println(num);
}
}
InnerClass2 i = new InnerClass2();
i.show();
}
}
/*
测试类
*/
public class OuterTest {
public static void main(String[] args) {
OuterClass2 o = new OuterClass2();
o.method();
}
}
输出:
10
(3)匿名内部类
是局部内部类的一种形式,所以属于局部内部类。
格式:
new 类名或接口名() {
重写方法...
}; //注意这里的分号
//例如
new AnonymousInnerClass() {
public void show() {
...
}
};
从上述定义来看,匿名内部类的本质其实就是一个对象 这里理解的不对,应该是说通过匿名内部类可以方便地创建对象,这个对象所属的类继承了Inter,或实现了Inter这个接口。再结合多态:
AnonymousInnerClass i = new AnonymousInnerClass() {
public void show() {
...
}
};
i.show();
具体实例:
//接口
public interface Inner {
void show();
}
public class OuterClass3 {
public void method() {
Inner i = new Inner() {
@Override
public void show() {
System.out.println("匿名内部类");
}
};
i.show();
}
}
/*
测试类
*/
public class OuterTest {
public static void main(String[] args) {
OuterClass3 o = new OuterClass3();
o.method();
}
}
输出:
匿名内部类
TODO:补充匿名内部类访问外部属性的限制,好像是只能是final的
(4)静态内部类
TODO
二、lambda表达式
2.1 Comparable接口
Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparable接口。
public interface Comparable
{
int comparaTo(Object other);
}
在JAVA 5中,Comparable接口已经提升为一个泛型类型。
public interface Comparable<T>
{
int comparaTo(T other);
}
现在,假设希望使用Arrays类的sort方法对Employee对象数组进行排序,Employee类就必须实现Comparable接口。
public class Employee implements Comparable<Employee>{
String name;
double salary;
//省略getter和setter和构造方法
// public int compareTo(Object o) {
// Employee other = (Employee) o;
// return Double.compare(salary, other.salary);
// }
//使用泛型接口后
public int compareTo(Employee o) {
return Double.compare(salary, o.salary);
}
}
为什么不能直接在Employee类提供一个compareTo方法,而必须实现Comparable接口呢?主要原因在于Java程序设计语言是一种强类型(strongly typed)语言。在调用方法的时候,编译器要能检查这个方法确实存在。
测试排序1
@Test
public void test() {
Employee[] employees = new Employee[3];
employees[0] = new Employee("a", 200);
employees[1] = new Employee("b", 100);
employees[2] = new Employee("c", 300);
Arrays.sort(employees);
for (Employee employee : employees) {
System.out.println(employee.getName());
}
//结果:b a c
}
2.2 Comparator接口
在前面,我们已经了解了如何对一个对象数组进行排序,前提是这些对象是实现了Comparable接口的类的实例。例如,可以对一个字符串数组进行排序,因为String类实现了Comparable,而且String.compareTo方法可以按字典顺序比较字符串。
但是现在我们希望按长度递增的顺序对字符串进行排序,肯定不能让String类用两种不同的方式实现compareTo方法,而且String类也不应由我们来修改。
要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(Comparator)作为参数,比较器是实现了Comparator接口的类的实例。
public interface Comparator<T>
{
int compare(T first, T second);
}
现在创建一个实现Comparator接口的类
public class LengthComparator implements Comparator<String> {
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
测试排序2
@Test
public void test() {
String[] friends = {"Peter", "Paul", "Mary12312321"};
LengthComparator comp = new LengthComparator();
//为sort方法传入一个LengthComparator对象
Arrays.sort(friends, comp);
for (String friend : friends) {
System.out.println(friend);
}
//结果 Paul Peter Mary12312321
}
2.3 匿名内部类
紧接上面学到的,这里就可以用上匿名内部类了。
回顾一下匿名内部类的定义:匿名内部类的本质其实就是一个对象 通过匿名内部类可以方便地创建一个对象,这个对象所属的类必须继承某个类或实现了某个接口。
从上面的例子2来看,比较只需要用到一次LengthComparator类,而sort方法又只需要“实现了Comparator接口的类的对象"即可,那么就适合用匿名内部类了。
public class Test {
public static void main(String[] args) {
String[] friends = {"Peter", "Paul", "Mary12312321"};
//为sort方法传入一个实现了Comparator接口的类的对象
Arrays.sort(friends, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
});
for (String friend : friends) {
System.out.println(friend);
}
//结果 Paul Peter Mary12312321
}
}
2.4 lambda表达式
从上面的例子2来看,可以向sort方法传入一个"实现了Comparator接口的类的对象",而这个接口”只有一个抽象方法“,所以属于”函数式接口“。
public class LengthComparator implements Comparator<String> {
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
···
Arrays.sort(strings, new LengthComparator());
这是将一个代码块传递到某个对象,这块代码来检查一个字符串是否比另一个字符串短。这里计算:
first.length - second.length();
first和second是什么?它们都是字符串。Java是一种强类型语言,所以我们还要指定它们的类型:
(String first, String second) -> first.length() - second.length();
这就是第一个lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。
-
Arrays.sort方法需要两个参数,一个是比较的对象数组,另一个是“实现了Comparator接口的对象“
-
lambda表达式的说明:对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式
-
所以sort的第二个参数,正好符合上述定义
所以测试2写成lambda表达式:
@Test
public void test() {
String[] friends = {"Peter", "Paul", "Mary12312321"};
Arrays.sort(friends, (first, second) -> first.length() - second.length());
for (String friend : friends) {
System.out.println(friend);
}
//结果 Paul Peter Mary12312321
}
三、函数式接口
函数式接口:只有一个抽象方法的接口,如上述的Comparator接口,只有一个抽象方法compare(T o1, T o2)
Java API在java.util.function包中定义了很多非常通用的函数式接口:
-
BiFunction<T, U, R>描述了参数类型为T和U,返回类型为R的函数;
-
Predicate{boolean test(T t); }
-
Supplier{T get(); }供应者用于实现懒计算
四、方法引用
有时,lambda表达式仅仅涉及一个方法,例如,假设希望只要出现一个定时器事件就打印这个事件对象:
var timer = new Timer(1000, event -> System.out.println(event));
但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:
var timer = new Timer(100, System.out::println);
表达式System.out::println是一个方法引用(method reference),它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,它的actionPerformed(ActionEvent e)方法要调用System.out.println(e)。
我的理解是:
在new Timer(1000, event -> System.out.println(event));这方法中,第二个参数本来是要接收一个实现了ActionListener接口的对象,这个对象重写了接口的actionPerformed(ActionEvent e)方法。由于这个接口是函数式接口,所以可以用lambda表达式代替。
然后,由于这个重写的方法里面,只调用一个方法而不做其他操作,所以可以把lambda表达式重写为方法引用。var timer = new Timer(100, System.out::println);
方法引用要使用::运算符分隔对象或类名与方法名。主要有3种情况:
- object::instanceMethod
- Class::instanceMethod
- Class::staticMethod
在第1种情况下,方法引用等价于向方法传递参数的lambda表达式。对于System.out::println,对象是System.out,所以方法表达式等价于x -> System.out.println(x);
对于第2种情况,第1个参数会成为方法的隐式参数,例如,String::compareToIgnoreCase等价于(x, y) -> x.compareToIgnoreCase(y);
在第3种情况下,所有参数都传递到静态方法,Math::pow等价于(x, y) -> Math.pow(x, y);
类型 | 语法 | 对应的lambda表达式 |
---|---|---|
静态方法引用 | 类名::staticMethod | (args) -> 类名.staticMethod(args) |
实例方法引用 | inst::instMethod | (args) -> inst.instMethod(args) |
对象方法引用 | 类名::instMethod | (inst,args) -> inst.instMethod(args) |
构建方法引用 | 类名::new | (args) -> new 类名(args) |
TODO:构造器引用
五、变量作用域
TODO