2020 年 6 月 7 日 接口、lambda 表达式与内部类
接口技术主要是用来描述类具有什么功能,并不给出每个功能的具体实现,一个类可以实现一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
lambda表达式,一种可以表示在将来某个时间点执行的代码块的简洁方法。
内部类,定义在另一个类的内部,其中方法可以访问包含它们外部类的域,内部类主要是用于设计具有相互协作关系的结合。
代理:这是一种实现任意接口的对象
1、接口
1、接口概念
接口不是类,,而是对类的一组需求描述,这些类需要遵从接口描述的统一格式进行定义。
接口中所有的方法都自动地属于public,因此,在接口中申明方法时,不必提供关键字public。
提供实例域和方法实现的任务,应该由实现接口的那个类来完成。因此可以将接口看成没有实例域的抽象类。
实现一个接口的步骤:
1、对类声明为实现给定的接口
2、对接口中的所有方法进行定义
2、接口特性
1、接口不是类,尤其不能使用new运算符实例化一个接口
2、接口变量必须引用实现了接口的类对象
3、可以使用instance检查一个对象是否实现了某个特定的接口。
4、接口中的域将被自动设为public static final。
3、接口与抽象类
1、每个类只能扩展一个类(只能继承一个类)
2、一个类可以实现多个接口
4、静态方法
接口中可以增加静态方法
5、默认方法
在接口中将一个方法用default修饰符进行修饰。
6、解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,Java的规则是:
1、超类优先。如果超类提供了一个具体方法,同名而且具有相同参数类型的默认方法会被忽略。
2、接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖方法来解决这个冲突。
3、如果一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。在这种情况下,只会考虑超类方法,接口的默认方法都会被忽略,这就是“类优先”规则。
2、接口示例
1、接口与回调
回调是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取什么动作。
例如Java中的定时器
Timer构造器的第一个参数是发出通告的时间间隔,他的单位是毫秒。第二个参数是监听器对象
2、Comparator接口
如果对一个对象数组排序:
1、这些对象是实现了Comparable接口的类的实例
2、利用Arrays.sort(数组,比较器)
3、对象克隆
要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。注意:原变量和副本都是同一个对象的引用。这说明任何一个变量改变时都会影响另一个变量。
如果希望copy一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,就需要使用clone()方法。
注意:clone方法是Object的一个protected方法,说明你的代码不能直接调用和这个方法,只有Employee类可以克隆Employee对象。默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。
通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象
对于每一个类需要确定:
1、默认的clone方法是否满足需求;
2、是否可以在改变的子对象上调用clone来修补模拟人生的clone方法;
3、是否不该使用clone
即使默认的clone方法能够满足需求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone();
3、lambda表达式
1、为什么引入lambda表达式
lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。
2、lambda表达式的语法
案例:(String first,String second )-> first.length()-second.length()
这就是一个lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。
参数,箭头(—>) 以及一个表达式
如果代码要完成的计算无法放在一个表达式中,就像写方法一样,把这些代码放在{}中,并包含显示的return语句。
3、函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口。
例如:
Arrays.sort(words, (first,second) ->first.length()-second.length() ).
在底层,Arrays.sort方法会接收实现了Comparable<String>的某个类的对象,在这个对象上调用compare方法会执行这个lambda表达式的体。
最好把lambda表达式看成一个函数而不是一个对象。
lambda表达式可以转换为接口,这一点让lambda表达式很具有吸引力,具体的语法很简短。
案例:
4、方法引用
案例:
Timer t =new Timer(1000,event -> System.out.println(event));
等价于
Timer t = new Timer(1000,System.out::println);
表达式System.out::println是一个方法引用,他等价于上面的lambda表达式。
方法引用需要用 :: 操作符分割方法名与对象或类名。主要有三种情况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
前两种情况,方法引用等价于提供方法参数的lambda表达式
对于第三种情况,第一个参数会成为方法的目标,例如:String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y);
5、构造器引用
构造器引用与方法引用很类似,只不过方法名为new。
例如:Person::new是Person构造器的一个引用
可以用数组类型建立构造器引用
例如:int[]::new 是一个构造器引用,它有一个参数:即数组长度。这等价于lambda表达式 x-> new int[x]。
6、变量作用域
lambda表达式有三个部分:
1、一个代码块
2、参数
3、自由变量值,这是指非参数而且不在代码中定义的变量
如果希望在咋lambda表达式中访问外围方法或类中的变量,需要注意:
1、在lambda表达式中,只能引用 值不会改变的变量,如果在lambda表达式中改变变量,并发执行多个操作时就会不安全。
2、如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的
3、lambda表达式中捕获的变量必须实际上是最终变量,实际上最终变量指的是这个变量初始化之后就不会在为它赋新值。
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
7、处理lambda表达式
使用lambda表达式的重点是延迟执行。
使用lambda表达式的场景:
1、在一个单独的线程中运行代码
2、多次运行代码
3、在算法的适当位置运行代码
4、发生某种行为时执行代码
5、只在必要时运行代码
要接收一个lambda表达式,需要选择一个函数式接口
4、内部类
内部类是定义在另一个类中的类。
使用内部类的原因:
内部类可以访问该类定义所在的作用域中的数据,包括私有数据
内部类可以对同一个包中的其他类隐藏起来
当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷
嵌套是一种类之间的关系,而不是对象之间的关系。嵌套有两个好处:命名控制和访问控制。
1、使用内部类访问对象状态
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。这个引用在内部类中定义是不可见的。
2、内部类的特殊语法
事实上,使用外围类引用的正规语法还要复杂一些。
表达式:OuterClass.this 表示外围类引用
例如:if(TalkingClock.this.beep) Toolkit.beep()
反过来,可以采用下列语法格式更加明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters);
例如:ActionListener listener = this.new TimePrinter();
需要注意的是:在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
内部类中声明的所有静态域都必须是final,原因很简单,我们希望一个静态域只有一个实例。内部类不能有static方法
3、内部类是否有用、必要和安全
尽量减少使用内部类
4、局部内部类
如果某个类只在方法中调用了一次,那么就可以在一个 方法中定义局部类。
局部类不能用public或private访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部的世界可以完全的隐藏起来
5、由外部方法访问变量
局部类还有一个特点。它们不仅能访问包含它们的外部类,还可以访问局部变量。不过那些局部变量必须事实上为final,这说明它们一旦赋值就绝对不会改变。
6、匿名内部类
将局部内部类在深入一步,假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类。
例如:
它的含义是创建一个实现ActionListener接口的类的对象,需要实现的方法actionPerformed定义在括号{}内。
由于构造器的名字必须与类名相同,而匿名类没有类名,所以匿名类不能有构造器。取而代之的是,将构造器参数传递给超类构造器。
7、静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消引用。
当然,只有内部类可以声明为static,静态内部类的对象除了没有对生成它的外围对象的引用特权外,与其他所有内部类完全一样。
在内部类不要访问外围类对象的时候,应该使用静态内部类。
5、代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
1、何时使用代理
假设有一个表示接口的Class对象,它确切类型再编译时无法知道。要想构造一个实现这些接口的类,就需要使用newInstance方法或反射找出这个类的构造器。但是不能实例化一个接口,需要再程序运行时定义一个新类。
为了解决这个问题,有些程序将会生成代码,将这些代码放置再一个文件中;调用编译器,然后再加载结果类文件。而代理机制是一种更好的解决方案,代理类可以再运行时创建全新的类。这样的代理能够实现指定的接口。尤其是,它具有以下方法:
1、指定接口所需要的全部方法
2、Object类中的全部方法,例如,toString(),equals()
然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器。调用处理器是实现了InvocationHandler接口的对象。
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的参数。
在本书的案例中,由于数组中填充了代理对象,所以compareTo调用了TranceHander类中的invoke方法,这个方法打印出来方法名和参数。之后用包装好的Integer对象调用compareTo。
2、创建代理对象
要想创建一个带来对象,需要使用Proxy类的newProxyIntance方法。这个方法有三个参数:
一个类加载器
一个Class对象数组,每个元素都是需要实现的接口
一个调用处理器
3、代理类的特征
代理类是在程序运行过程中创建的。然而一旦创建就变成了常规类,与虚拟机中的任何其他类没有区别。
所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象职责,所需要的任何附加参数都必须存储在调用处理器中。
代理类一定是public 和final。如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则,所有非共有的接口都必须属于同一个包,同时代理类也属于这个包。
版权声明: 本文为 InfoQ 作者【瑞克与莫迪】的原创文章。
原文链接:【http://xie.infoq.cn/article/14631e820ea6b063187b43212】。文章转载请联系作者。
评论