03.接口与lambda表达式
03.接口与lambda表达式
3.1 接口
- 在Java程序设计语言中,接口不是类,而是对希望符合的这个类的一组需求,例如:
Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提对象所属的类必须实现了Comparable接口。下面是Comparable接口的代码:
//在java5之前的实现方法,现已提升为泛型Comparable<T>
public interface Comparable
{
int compareTo(Object other);
}这就是说,任何实现Comparable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值
- 接口的实现步骤:1️⃣将类声明为实现给定的接口(使用
implements关键字);2️⃣对接口中所有方法提供定义(在接口中未声明方法为public是因为接口中方法必为public,在实现时不可省略,因为类的默认访问属性是什么都不写,编译会报错)
//Comparable接口实现(建议CompareTo方法与equals方法兼容,结果保持一致)
class Employee implements Comparable{
public int compareTo(Object otherObject){
Employee other = (Employee) otherObject;
return Double ,compare(sal ary, other,sal ary);
}
}
//我们可以做得更好一些。可以为泛型 Comparable 接口提供一个类型参数
class Employee implements Coniparable<Employee>
{
public int compareTo(Employee otherObject){
return Double.compare(sal ary, other.sal ary);
}
}- 接口的属性
- 接口不是类不能被实例化
- 可以用anObject intstanceof Comparable一样判断类是否扩展了该接口
- 接口和继承层次一样,也可以从多用性高的拓展到专用性高的接口
- 接口中的字段总是public static final(有些接口只定义常量),java规范建议不要通过多余的关键字
- 每个类允许扩展多个接口,接口间用
,隔开
- 接口与抽象类关系
- 为什么不把接口设计为抽象类:使用抽象类表示通用属性有一个问题,类只能扩展一个类,既然java没有引入多重继承,就提供了接口,事实上,接口可以提供多重继承的大部分好处,也没有多重继承复杂性和低效性
C++有多重继承,但很少有人使用,有人甚至认为不应该使用,也有人认为应该像java一样使用
在java8中,允许给接口定义静态方法,之前都是放在一个伴随类里,这样有时伴随类就不必要了,在java9中,接口中方法可以是
private,但它们只能在接口中被使用,作为接口基本方法的辅助方法默认方法:可以为接口方法定义一个默认实现,使用
default修饰,默认方法可以调用其它接口中还未的方法,默认方法可以用于接口演化,可以使扩展的类不会因为没有实现新写入接口的方法,而出现AbstractMethodError错误接口默认方法冲突解决 1️⃣ 如果是与超类方法冲突,以超类方法为准,接口默认方法会被忽略 2️⃣ 如果都是接口方法,必须重写这个方法,才能使用(没有提供默认写法则没事)
千万不要让接口重新定义Object类的方法,由于类优先原则,这样的方法无法使用
接口与回调
回调是一种设计模式,可以指定某种事物发生时采取的某种特定的动作
比如:写一个定时通知的时钟,在很多语言中,这种情况可以通过定时调用完成,但是在java是类采用的是面向对象方法,可以向定时器传入某个类对象,然后定时器调用这个类的方法,由于对象可以有信息,因此更灵活,当然定时器需要知道调用哪个方法,并要求类实现了ActionListener接口
cloneable接口:由于Object类的clone是浅拷贝(对于类内部的类对象没有拷贝),用于安全的克隆方法,不太常见且细节技术性过强,需要点这里
cloneable接口是java中提供的少数标记接口之一,标记接口中不含任何方法,它唯一的作用就是允许在类查询使用instanceof建议你不要使用标记接口
3.2 lambda表达式
- lambda表达式(λ)是一种可传递的代码块,可以在以后执行一次或多次
- 例如之前使用接口完成了在指定间隔下完成工作,一个代码块将会传递给一个定时器,这个代码块会在将来某个时间调用
- 但目前为止,由于java的代码都在类里,不容易传递代码段,只能构造传递对象
- lambda是java之后找到的一种适合java的传递方法
lambda表达式的语法
lambda的定义:我们举比较字符串长度例子,这里要计算
first.length() - second.length()first和second都是字符串,java是强类型语言,要标明类型,因此,应该这样写,(String first,String second) -> first.length() - second.length(),这就是一个lambda表达式,由参数、箭头和一些表达式组成(没有参数一定要打括号,并且符合函数,有返回值必须全部分支都有),这样的表达式能作为参数传入函数这个表达式可以通过接口对象接收,代替
函数接口:只有一个抽象方法的接口叫函数接口,需要扩展了它的对象时,就可以提供一个lambda表达式,例如:Arrays.sort()方法第二个参数需要一个实现了Comparator接口(只有一个方法)的实例,所以可以使用lambda表达式当这个实例
Arrays.sort(words,(first,second) -> first.length() - second.length());,由于Comparator是作用于String上,所以可以不用声明类型
lambda所能做的也只是转换为函数式的接口,不像Python,java最后没有增加函数类型
- 方法引用:可以将方法作为lambda表达式参数,例如:可以使用System.out::println传入println方法,甚至可以是构造器方法,至于是哪一个,取决于上下文,这需通过map、collect和stream方法才能使用
- lambda表达式使用的变量作用域:由于lambda会在某一时间执行,这可能比传入参数晚很多,这是函数局部参数变量可能不在了,所以lambda表达式会捕获需要的参数,它可以捕获一个引用值不会改变的变量,即不能在lambda改变引用的数值,因为这样在并发时可能不安全,这个值哪怕在外面被改变都是不行的,即捕获的必须是事实最终变量(effectively final),不会变化
- lambda表达式里可以使用this参数,但指的是创建该表达式的类的this
- lambda表达式的处理
- lambda由一个参数表,一个代码块和一个事实最终变量(非参数且在外部定义的最终变量)组成,如果你希望代码以后执行,就可以用这个方法,然后处理
- 一个简单的例子:一个reapeat方法,
repeat(10,->System.out.println("Hello,World!"));,要接受传入的lambda,需要选择一个函数式接口,在javaAPI提供了重要的函数式接口(见下图),应该在函数定义时使用它,如果我们使用Runnable接口,就应该这样定义函数repeat:
public static void repeat(int n,Runnable action){
for(int i = 0;i < n;i++){ action.run(); }
}
- 在上方例子中,调用函数接口的run()方法就会,执行lambda代码,这个方法无参无返回值,如果希望返回或者接受值,使用其他函数接口
- 合并函数,使用这些函数等同于谓词and、or和negate例如,
Predicate.isEqual(a).or(Predicate.isEqual(b))就等同于x -> a.equals(x) || b.equals(x) - 特殊化接口,有时会更高效:

如果设计你自己的接口,其中只有一个抽象方法,可以用
@FunctionalInterface注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外javadoc页里会指出你的接口是一个函数式接口。 并非必须使用这个注释,任何只有一个抽象方法的都是函数接口,不过使用是个好主意
- 内部类是定义在类中的类,这个类可以对同包内的其他类隐藏,并且可以访问定义类的这个类的作用域内的数据,包括原本的私有数据,与C++的嵌套类类似,但更丰富更有用,虽然有些方面已经被lambda表达式代替(回调数据),但依旧很有用
- 使用内部类访问对象状态,内部类总有一个隐式访问
外部类名.this指向外部类,可以使用这个访问外部类方法和字段(包括private部分) - 内部类的访问属性,内部类可以为private(只有外部构造的类可以构造该类对象),外部类和内部类都可以为public和无修饰,内部类的静态字段只能为final,而且直接初始化,内部类不能有静态方法
- 内部类的安全性:当java1.1版本增加了内部类,这很重要,但是违背了java比C++简单的设计理念,这是否没必要,使用javap工具,会发现编译器会将内部类与外部类的代码在编译时使用$隔开,而虚拟机一无所知,它会在编译时自动转换,转换成名字古怪的常规类而且如果需要访问外部类的私有字段,又不愿在外部类定义,是做不到用其他类访问的。
- 这样做的确存在安全问题,任何人都可以通过调用这个转化的常规类的方法访问外部类,熟悉java文件结构的黑客可以使用16进制编辑器创建一个类文件,利用虚拟机指令调用那个方法,并且将攻击代码放入外部类同一个包里,但这样做需要技巧和决心,普通程序员不可能在无意中做到,必须刻意修改
局部类:如果一个内部类只出现一次,可以在方法中定义这个局部类,这时不能使用访问权限修饰符修饰这个类,它的声明被限制在这个代码块内,如果使用这种声明方法,它还可以访问方法中的事实最终局部变量
- 这个类甚至可以不设计名字,使用匿名的设计,如果你只需要创建一个该类型的对象,就可以使用这种方法
- 匿名定义:
var 类对象名 = new 超类类名或接口名(构造参数){ 函数代码块部分和其他类定义 },它会继承超类或接口,这是普通内部类所不具有的 - 在匿名类中不能定义构造器,如果需要初始化,可以使用初始化块,实际上,构造参数(括号中的内容)会传递给超类的构造器
双括号初始化:使用匿名内部类一样的方法进行初始化,在后方使用
{{初始化块部分}},进行初始化,这个技巧很少使用匿名子类与外部类的派生类的不同:在前面,定义equals方法时,会先判断两个类是否属于同一个类(使用getClass方法),但是使用
if(getClass() != other.getClass())return false;对匿名子类判断会失败在静态方法中使用getClass方法也会失败,因为静态方法没有this,应该创建一个Object类的匿名子类对象使用getClass方法
new Object(){}.getClass().getEnclosingClass();(再对返回的class类处理得到外围类)静态内部类,有时候只是为了把一个类隐藏在另一个类内部,而不需要对外部类进行处理,就可以使用static声明,使用后不会生成外部类的this引用,并且可以有静态方法和字段,这个类的对象必须在外部类的静态方法中返回生成,例如:可以使用静态类完成同时计算数组的最大值最小值
在接口中声明的内部类自动是static和public
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 = values[0],max = values[0];
for(double value:values){
if(min > value){
min = value;
}
if(max < value){
max = value;
}
}
return new pair(min,max);
}
}
public class StaticInnerClassTest{
public static void main(String arg[]){
var values = new double[20];
for(int i = 0; i < values.length;i++){
values[i] = 100 * Math.random();
}
ArrayAlg.pair p = ArrayAlg.minmax(values);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}3.3 服务加载器
- 有时开发采用服务架构的应用,有些平台对这种方法提供支持,比如OSGi,可以用于开发环境、应用服务器和其他复杂的应用。在jdk提供了一个加载服务的简单机制,由java平台模块提供支持
- 实现方法:
- 定义一个接口或超类,写入包含服务的各个实例应该要提供的方法,例如可以定义一个Cipher接口,写入需要的方法
- 用多个类扩展(继承)它,这些类可以放在不同的包里,但需要定义无参构造器
- 初始化一个服务加载器,
ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher.class),这个行为只会进行一次,ServiceLoader可以很容易地加载符合公共接口的服务 - 定义方法,在服务加载器(可迭代对象,通过for-each遍历处理完成,也可通过流完成)中寻找可以完成服务的对象
3.4 代理
- 代理可以在运行时创建一组给定接口的新类,这很少见,但有时重要
- 想要创建一个代理对象,需要使用Proxy类的newProxyInstance方法,这个方法有三个参数
- 一个类加载器
- 一个class数组,对应需要实现的接口
- 一个调用处理器(一个实现了InvocationHandler接口的对象,这个接口只有一个方法:
Object invoke(Object proxy,Method method,Object[] args),这个方法会传递一个Method对象和调用的参数)
- 代理用于处理什么问题
- 将方法调用路由到远程服务器
- 运行程序中将用户界面事件与动作关联(例如:在一些类执行时给予提示)
- 为了调试,追踪方法调用
- 代理的使用,普通的代理,直接建立一个类,这个类中有另一个类的对象,如果需要就创建,可以增加其他方法,这就是最简单的代理(静态),每次出现新的需求,需要写新方法进行处理,下面是严格遵循接口的动态代理,这种代理,无论要代理对象如何变化,只要是针对该类的所有方法,都是统一处理的
- 创建一个实现
InvocationHandler接口的类,其中有需要代理的类的实例,初始化时需要代理的实例会赋值给它,这个类被称为调用处理器,在代理类执行操作时,会自动调用invoke方法。 - 重写该类的
invoke方法,这个方法重写的关键在于在执行时,在中间执行传入method.invoke方法,并将其invoke方法返回值返回,下面写的是查看需要代理对象的方法运行时间的调用处理器 - 这时,如果需要代理的类为
ICoder,它需要在每次操作时,查看前后时间,定义好该类 - 在程序中,创建该类对象,再定义
InvocationHandler对象指向调用处理器 - 获得要代理对象的类加载器ClassLoader的对象(负责加载Java类到Java虚拟机中)
- 通过类加载器得到一个代理类,通过Proxy.newProxyInstance(类加载器,实现了的接口,调用处理器)方法定义(绑定之前初始化的调用接口处理器,初始化为被代理的实例)
- 使用代理类执行方法,就会收到代理响应
网上总结的:
,下方是网上的例子
class CoderDynamicProxy implements InvocationHandler{
//被代理的实例
private ICoder coder;
public CoderDynamicProxy(ICoder _coder){
this.coder = _coder;
}
//调用被代理的方法
//当我们调用代理类对象的方法时,这个“调用”会转送到中介类的invoke方法中
//参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(System.currentTimeMillis());
Object result = method.invoke(coder, args);
System.out.println(System.currentTimeMillis());
return result;
}
}
public class DynamicClient {
public static void main(String args[]){
//要代理的真实对象
ICoder coder = new JavaCoder("Zhang");
//创建中介类实例
InvocationHandler handler = new CoderDynamicProxy(coder);
//获取类加载器
ClassLoader cl = coder.getClass().getClassLoader();
//动态产生一个代理类
ICoder proxy = (ICoder) Proxy.newProxyInstance(cl, coder.getClass().getInterfaces(), handler);
//通过代理类,执行方法,在执行前后出现时间显示
proxy.do("Modify user management");
}
}3.5 反射、lambda与代理
- 反射:通过使用
java.lang.reflect包中的类,和可获取到的class对象,对程序进行处理响应,使程序能判断自身类,是对类的属性进行判断 - lambda表达式:通过定义需要的函数接口的lambda对象,这个对象可由已有函数引用初始化,也可自定义函数代码块,达到将类函数代码块传递的目的,执行函数接口的函数就会执行代码块,是传递代码块
- 代理:通过使用反射的Method类和代理的InvocationHandler接口、Proxy类,通过一个实例,产生一个代理类,使用这个代理类的每一个方法都会执行相同的预订操作,是在操作类时给出提示或状态显示
