第5章 接口、lambda表达式与内部类
5.1 接口
5.1.1 接口的基础语法
- 接口我们可以看作是抽象类的一种特殊情况,在接口中只能定义抽象的方法和常量。
- 接口是一种“引用数据类型”,编译之后也是一个class字节码文件。
- 接口怎么定义:[修饰符列表] interface 接口名{}。
- 接口支持多继承,一个接口可以继承多个接口。接口之间可以继承,但接口之间不能实现。
- 接口中只有常量+抽象方法。其所有元素都是public修饰的。
- 接口中的方法默认都是 public abstract 的,不能更改,但可以省略。
- 接口中的变量默认都是 public static final 类型的,不能更改(可省略),所以必须显式的初始化。
- 接口不能被实例化,接口中没有构造函数的概念,接口中方法不能有方法体(JDK8中可以有方法体)。
- 接口中的方法只能通过类来实现,通过implements 关键字。
- 如果一个类实现了接口,那么接口中所有的方法必须实现。
- 一类可以实现多个接口,用逗号隔开。
- extends和implements可以共存,extends在前,implements在后。
- 使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)。
5.1.2 接口在开发中作用
例如,Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparable接口。下面是Comparable接口的代码:
public interface Compare<T>{
int compareTo(T o);
}
这说明,任何实现Comparable接口的类都需要包含compareTo方法,这个方法有一个Object参数,并且返回一个整数。假设希望根据员工的薪水进行比较,以下是compareTo方法的实现:
class Employee implements Comparable<Employee>{
public int compareTo(Employee other){
return Double.compare(salary, other.salary);
}
}
要让一个类使用排序服务必须让他实现compareTo方法。这是理所当然的,因为要向sort方法提供对象的比较方法。但是为什么不能再Employee类中直接提供一个compareTo方法,而必须实现Comparable接口呢?
主要原因在于Java程序设计语言是一种强类型语言。在调用方法的时候,编译器要能检查这个方法确实存在。
注意:Comparable接口的文档建议compareTo方法应该与equals方法兼容。也就是说,当x.equals(y)时x.compareTo(y)就应该等于0。JavaAPI中大多数实现Comparable接口的类都遵从了这个建议。不过又一个重要的例外,就是BigDecimal。
在Java中接口其实描述了类需要做的事情,类需要做的事情,类要遵循接口的定义来做事,使用接口到底有什么本质的好处?可以归纳为三点:
- 采用接口明确的声明了它所能提供的服务。
- 解决了Java单继承的问题。
- 实现了可接插信(低耦合度)。
5.1.3 接口和抽象类的区别
- 接口描述了方法的特征,不给出实现,解决 java 的单继承问题,实现了强大的可接插性。
- 抽象类提供了部分实现,抽象类是不能实例化的,抽象类的存在主要是可以把公共的代码移植到抽象类中。
- 面向接口编程,而不要面向具体编程(面向抽象编程,而不要面向具体编程)。
- 优先选择接口(因为继承抽象类后,此类将无法再继承,所以会丧失此类的灵活性)。
- 抽象类是半抽象的;接口是完全抽象的。
- 抽象类中有构造方法;接口中没有构造方法。
- 接口和接口之间支持多继承;类和类之间只能单继承。
- 一个类可以同时实现多个接口;一个抽象类只能继承一个类
- 接口中只允许出现常量和抽象方法。
5.1.4 静态和私有方法(了解)
在Java8中,允许在接口中增加静态方法。在标准库中,会看到成对出现的接口和实用工具类,如Collection/Collections。在Java9中,接口中的方法可以是private。private方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。
5.1.5 接口的属性
如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口。接口不是类。具体来说,不能使用new运算符实例化一个接口。不过,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x;
接下来,如果使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口:
if(anObject instanceOf Comparable){...}
与建立类的继承层次一样,也可以扩展接口。这里允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。
5.1.7 Comparator接口
假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法。要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。
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();
}
}
具体完成比较时,需要建立一个实例:
LengthComparator comp = new LengthComparator();
if(comp.compare(words[i], words[j]) > 0){...}
将这个调用与“word[i].compareTo(words[j]);”进行比较。这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。要对一个数组排序,需要为Arrays.sort方法传入一个LengthComparator对象:
String[] friends = {“Peter”, “Paul”, “Mary”};
Arrays.sort(friends, new LengthComparator());
现在这个数组可能是[“Paul”,”Mary”,”Peter”]或[“Mary”,”Paul”,”Peter”]。
5.2 lambda表达式
5.2.1 为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。在Java中传递一个代码段并不容易,不能直接传递代码段。Java是一种面对对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需要的代码。就像下面的代码一样:
public class LambdaDemo {
//函数定义
public void printSomething(String something){
System.out.println(something);
}
//通过创建对象调用函数
public static void main(String[] args) {
LambdaDemo demo = new LambdaDemo();
String something = "一般调用模式";
demo.printSomething(something);
}
}
这是经典OOP的实现样式。下面我们对上面的代码做一个修改,创建一个功能接口,并对该接口定义抽象方法。
public class LambdaDemo {
//抽象功能接口
interface Printer {
void print(String val);
}
//通过参数传递功能接口
public void printSomething(String something, Printer p) {
p.print(something);
}
public static void main(String[] args) {
LambdaDemo demo = new LambdaDemo();
String something = "传统的接口函数实现方式";
//注意是实现Printer接口,而不是new了Printer,大括号很重要
Printer p = new Printer() {
@Override
public void print(String val) {System.out.println(val);}
};
demo.printSomething(something, p);
}
}
而lambda它将函数式编程概念引入Java,函数式编程的好处在于可以帮助我们节省大量的代码,非常方便易用,能够大幅度的提高我们的编码效率。
至此我们都尚未使用lambda表达式。我们仅创建了Printer接口的具体实现,并将其传递给printSomething方法。
首先我们知道lambda表达式,表达的是接口函数,箭头左侧是函数的逗号分隔的形式参数列表,箭头右侧是函数体代码。现在,我们使用lambda表达式重构下之前的代码。
public class LambdaDemo {
//抽象功能接口
interface Printer {
void print(String val);
}
//通过参数传递功能接口
public void printSomething(String something, Printer printer) {
printer.print(something);
}
public static void main(String[] args) {
LambdaDemo demo = new LambdaDemo();
String something = "lambda表达式实现方式";
//实现Printer接口(请关注下面这行lambda表达式代码)
Printer p= (String toPrint)->{System.out.println(toPrint);};
//调用接口打印
demo.printSomething(something, p);
}
}
lambda表达式使我们代码更简洁。对比传统java代码的实现方式,代码量减少了很多。但这仍然不是最简的实现方式,我们一步一步来。
public class LambdaDemo {
//抽象功能接口
interface Printer {
void print(String val);
}
//通过参数传递功能接口
public void printSomething(String something, Printer printer) {
printer.print(something);
}
public static void main(String[] args) {
LambdaDemo demo = new LambdaDemo();
String something = "lambda表达式实现方式";
//实现Printer接口(请关注下面这行lambda表达式代码)
Printer printer1 = (String val)->{System.out.println(val);};
demo.printSomething(something, printer1);
//简化:去掉参数类型
Printer printer2 = (b)->{System.out.println(b);};
demo.printSomething(something, printer2);
//简化:去掉参数括号
Printer printer3 = c->{System.out.println(c);};
demo.printSomething(something, printer3);
//简化:去掉函数体花括号
Printer printer4 = d->System.out.println(d);
demo.printSomething(something, printer4);
}
}
即使没有在箭头的左侧指定参数的类型,编译器也会从接口方法的形式参数中推断出其类型。那么,我们最终通过lambda表达式,简化完成的代码:
public class LambdaDemo {
interface Printer {
void print(String val);
}
public void printSomething(String something, Printer printer) {
printer.print(something);
}
public static void main(String[] args) {
LambdaDemo demo = new LambdaDemo();
String str = "lambda表达式函数式接口";
demo.printSomething(str, a -> System.out.println(a));
}
}
lambda表达式表达的是接口函数,箭头左侧是函数参数,箭头右侧是函数体。函数的参数类型和返回值类型都可以省略,程序会根据接口定义的上下文自动确定数据类型。
(param1,param2,param3 ...,paramN)- > { //代码块; }
语法格式一:无参数,无返回值
() -> System.out.println(“Hello Lambda!”);
语法格式二:有一个参数,无返回值
(x) -> System.out.println(x);
语法格式三:若只有一个参数,小括号可以省略不写
x -> System.out.println(“Hello Lambda!”);
语法格式四:有两个以上的参数,有返回值,并且Lambda体中有多条语句
Comparator<Integer> com = (Integer x, Integer y) -> {
System.out.println(“函数式接口”);
return;
}
public class LambdaTest {
public static void printResult(Integer x1, Integer x2, Comparator<Integer> c ){
c.compare(x1, x2);
}
public static void main(String[] args) {
int a = 10000;
int b = 2000;
printResult(a, b, ((x1, x2) -> {
if (x1 > x2){
System.out.println("较大者:" + x1);
return x1;
} else if (x1 == x2){
System.out.println("两数相等");
return 0;
} else {
System.out.println("较大者:" + x2);
return x2;
}
}));
}
}
语法格式五:若Lambda体中只有一条语句,return和大括号都可以省略不写
Comparator<Integer> com = (Integer x, Integer y) -> Integer.compare(x,y);
语法格式六:lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器可以通过上下文推断出数据类型,即“类型推断”。
Comparator<Integer> com = (x, y) -> Integer.compare(x,y);
5.2.2 内置的四大核心函数式接口
1.Consumer<T>消费型接口:void accept<T t>;
public void happy(double money, Consumer<Double> con){
con.accept(money);
}
public void test1(){
happy(10000.11, m -> System.out.println("消费了" + m + "元"));
}
2.Supplier<T>供给型接口:T get();
public List<Integer> getNumList(int num, Supplier<Integer> sup){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < num; i++){
Integer n = sup.get();
list.add(n);
}
return list;
}
public void test2(){
List<Integer> numList = getNumList(10, () -> (int) (Math.random() * 100));
for (Integer n : numList){
System.out.println(n);
}
}
3.Function<T, R>函数型接口:R apply(T r);
public String strHandler(String str, Function<String, String>fun){
return fun.apply(str);
}
public void test3(){
String newStr = strHandler("\t\t\tlambda表达式",(str) -> str.trim());
System.out.println(newStr); //lambda表达式
String subStr = strHandler("lambda表达式", (str) -> str.substring(2,8));
System.out.println(subStr); //lambda
}
4.Predicate<T>判断型接口:boolean test(T t);
public List<String> filterStr(List<String> list, Predicate<String> pre){
List<String> strList = new ArrayList<>();
for (String str : list){
if (pre.test(str)){
strList.add(str);
}
}
return strList;
}
public void test4(){
List<String> list = Arrays.asList("Hello", "Lambda", "www", "OK");
List<String> stringList = filterStr(list, (str) -> str.length() > 3);
for (String s : stringList){
System.out.println(s);
}
}
5.2.4 变量作用域
- 在lambda表达式中,只能引用值不会改变的变量。如果在lambda表达式中更改变量,并发执行多个动作时就会不安全。
- 如果在lambda表达式中引用一个变量。而这个变量可能在外部改变,这也是不合法的。
- lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final)。
- lambda表达式的体与嵌套块有相同的作用域。这里同样适用明明冲突和遮蔽的有关规则。在lambda表达式中声明一个局部变量同名的参数或者局部变量是不合法的。
- 在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
5.2.5 Comparator
Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
静态comparing方法提取一个“键提取器”函数,他将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象进行排序:
Arrays.sort(people, Comparator.comparing(Person::getName));
与手动实现一个Comparator相比,这当然要容易得多。另外,代码也更为清晰。可以把比较器与thenComparing方法串起来,来处理比较结果相同的情况。例如:
Arrays.sort(people,Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName));
如果两人的姓相同,就会使用第二个比较器。这些方法有很多变体形式,可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:
Arrays.sort(people, Comparator.comparing(Person::getName, (s, t) -> Integer.compare(s.length() - t.length())));
5.3 内部类
在一个类的内部定义的类,称为内部类,主要分类:①实例内部类;②局部内部类;③静态内部类。
为什么需要使用内部类呢?主要有两个原因:
- 内部类可以对同一个包中的其他类隐藏。
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据。
内部类原先对于简洁地实现回调非常重要,不过如今lambda表达式在这方面可以做得更好。但内部类对于构建代码还是很有用的。使用内部类编写的代码,可读性很差。能不用尽量不用。
5.3.1 实例内部类
- 创建实例内部类,外部类的实例必须已经创建
- 实例内部类会持有外部类的引用
- 实例内部不能定义 static 成员,只能定义实例成员
public class InnerClassTest01 {
private int a;
private int b;
InnerClassTest01(int a, int b) {
this.a = a;
this.b = b;
}
//内部类可以使用 private 和 protected 修饰
private class Inner1 {
int i1 = 0;
int i4 = b;
}
public static void main(String[] args) {
InnerClassTest01.Inner1 inner1 = new InnerClassTest01(100, 200).new Inner1();
System.out.println(inner1.i1); //0
System.out.println(inner1.i4); //200
}
}
5.3.4 静态内部类
- 静态内部类不会持有外部的类的引用,创建时可以不用创建外部类
- 静态内部类可以访问外部的静态变量,如果访问外部类的成员变量必须通过外部类的实例访问
public class InnerClassTest02 {
static int a = 200;
int b = 300;
static class Inner2 {
//在静态内部类中可以定义实例变量
int i1 = 10;
//可以定义静态变量
static int i3 = 100;
//可以直接使用外部类的静态变量
static int i4 = a;
//不能直接引用外部类的实例变量
//int i5 = b;
//采用外部类的引用可以取得成员变量的值
int i5 = new InnerClassTest02().b;
}
public static void main(String[] args) {
InnerClassTest02.Inner2 inner = new InnerClassTest02.Inner2();
System.out.println(inner.i5); //300
}
}
5.3.5 局部内部类
局部内部类是在方法中定义的,它只能在当前方法中使用,和局部变量的作用一样。局部内部类和实例内部类一致,不能包含静态成员。
public class InnerClassTest03 {
private int a = 100;
//局部变量,在内部类中使用必须采用 final 修饰
public void method1(final int temp) {
class Inner3 {
int i1 = 10;
//可以访问外部类的成员变量
int i2 = a;
int i3 = temp;
}
//使用内部类
Inner3 inner3 = new Inner3();
System.out.println(inner3.i1);//10
System.out.println(inner3.i2);//100
System.out.println(inner3.i3);//300
}
public static void main(String[] args) {
InnerClassTest03 i = new InnerClassTest03();
i.method1(300);
}
}
5.3.6 匿名内部类
使用局部内部类时,通常还可以再进一步。假设只想创建这个类的一个对象,甚至不需要为类指定名字。这样一个类被称为匿名内部类。
interface MyInterface {
public void add();
}
public class InnerClassTest05 {
private void method(MyInterface myInterface) {
myInterface.add();
}
public static void main(String[] args) {
InnerClassTest05 innerClassTest05 = new InnerClassTest05();
innerClassTest05.method(new MyInterface() {
public void add() {
System.out.println("-------add------");
}
});
}
}
一般地,语法如下:
new SuperType(结构参数){
//内部类方法和数据
}
其中,SuperType可以是接口,如ActionListener,如果是这样,内部类就要实现这个接口。SuperType也可以是一个类,如果是这样,内部类就要扩展这个类。
由于构造器的名字必须与类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器。实际上,构造参数要传递给超类构造器。具体地,只要内部类实现一个接口,就不能有任何构造参数。不过,仍然要提供一组小括号。尽管匿名内部类不能有构造器,但可以提供一个对象初始化块。