Java chapter6 接口,Lambda表达式,内部类,Service Loaders 代理Proxy
6.1接口
例,排序算法需要类实现比较接口。
public interface Comparable{
int compareTo(Object other);
}
泛型
public interface Comparable<T>
{
int compareTo(T other);//parameter has type T
}
例Employee类实现接口
int compareTo(Employee other);
接口里的方法默认是Public的。
注意接口是没有实例的。
定义:
class Employee implements Comparable
public int compareTo(Object otherObject){
Employee other=(Employee)otherObject;
return Double.compare(salary,other.salary);
}
或
class Employee implements Comparable<Employee>{
public int compareTo(Employee other){
return Double.compare(salary,other.salary);
}
}
6.1.2接口属性
接口不是类,无法使用New操作符
可以定义接口类型变量
Comparable x;//OK
接口变量必须引用一个实现了该接口的类的对象
x=new Employee(...);//OK provided Employee implements Comparable
可以使用Insanceof来判断一个对象是否实现了接口
if(anObject instanceof Comparable){...}
接口可继承
public interface Movable{
void move(double x,double y);
}
pulbic interface Powered extends Movable{
double milesPerGallon();
}
接口不允许有实例作为域,但可以有常数
public interface Powered extends Movable{
double SPEED_LIMIT=95;//public static final constant
}
多个接口的实现
class Employee implements Cloneable,Comparable
6.1.4
接口可以有静态方法,私有方法。
私有方法只能被接口自身调用,一般作为其他函数的辅助函数。
6.1.5 默认方法
public interface Comparable<T>
{
default int compareTo(T other){return 0;}
}
default 方法可以调用其他的方法
public interface Collection{
int size();//抽象方法
default boolean isEmpty(){return size()==0;}
}
一个重要的使用default方法的地方是接口演化。
public class Bag implements Collection
Java8中 stream方法被加入了这个接口中。
如果stream 方法不是默认方法,Bag 类无法编译,因为它没有实现stream方法。
你可以使用老版本的jar,那么可以编译通过。
但是,如果一个程序调用Big类实例的stream方法,会抛出AbstractMethodError。
将方法前添加default会解决这个问题。如果Bag实例的stream调用,他会调用Collection.stream方法。
6.1.6解决默认方法的冲突
1.父类获胜,如果一个父类提供了明确的方法,拥有同名和同样参数列表的方法会被忽略,父类的方法会被调用。
2.接口崩溃,如果两个接口提供了相同的同名同参数的方法,你必须覆盖方法解决冲突。
interface Person{
default String getName(){return "";}
}
interface Named{
default String getName(){
return getClass().getName()+"_"+hashCode();
}
如果使用
class Student implements Person,Named{...}
}
这个类继承了两个接口中的getName方法,你必须覆盖代码。
class Student implements Person,Named{
public String getName(){return Person.super.getName();}
}
如果两个类中有一个类不是default,依然会冲突。需要覆盖方法。
当父类和接口同时有某个方法同名同参数,父类方法获胜。(java7 later)
你不能default Object类中的方法,比如toString equals
6.2 Lambda 表达式
(String first,String second)
->first.length()-second.length()
例
(String first,String second)->{
if(first.Length()<second.Length())return -1;
else if(first.Length()>second.Length())return 1;
else return 0;
}
如果一个lambda表达式没有参数,你可用()代替
()->{for(int i=100;i>=10;i--)System.out.println(i);}
lambda表达式的参数可推断,可写为
Comparator<String> comp
=(first,second)
->first.Length()-second.Length();
6.2.3功能接口
你可以提供一个lambda表达式当接口拥有一个抽象方法时,这个接口就被称为功能接口。
Array.sort()方法第二个参数需要一个Comparator的实例,这是一个只有一个方法的接口。我们有lambda实现
Arrays.sort(words,
(first,second)->first.length()-second.length());
6.2.4方法引用
假设你要打印一个timer
var timer=new Timer(1000,event->
System.out.println(event));
你可以将println方法传给Timer的构造函数
var timer=new Timer(1000,System.out::println);
System.out::println被称为方法引用。它引起编译器产生了一个方法接口的实例。覆盖了唯一的接口抽象方法。
另一例:
Arrays.sort(strings,String::compareToIgnoreCase);
使用方法引用的一列:
list.removeIf(Objects::isNull);// list.removeIf(e->e==null);
6.2.5构造器引用
构造器引用和方法引用很相似。只不过方法名为new,Person::new就是Person构造器的构造器引用。是哪一个构造器取决于上下文。
ArrayList<String> names=...;
Stream<Person> stream=new names.stream().map(Person::new);
List<Person> people=stream.toList();
map函数会对每一个ArrayList元素调用Person(String)构造器。如果Person有多个构造器,那么编译器会挑选那个只有一个String作为参数的构造器。
你可以对数组类型使用构造器引用。int[]::new相当于n->new int[n];
数组构造器引用对于克服Java的一些限制非常有用。如果我们想获得一个Person类对象的数组,Stream接口有一个toArray方法会返回Object数组
Object[] people=stream.toArray();
但是这不会让我们满意,我们需要一个Person的引用而不是Object的引用。我们这里解决方法就是构造器引用
Person[] people=stream.toArray(Person[]::new);
6.2.6变量范围
你经常想要获得lambda表达式内部的变量。如
public static void repeatMessage(String text,int delay){
ActionListener listener =event->
{
System.out.println(text);//
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay ,listener).start();
}
repeatMessage("Hello",1000);
注意text变量,text变量是函数的参数,不是lambda表达式内部的定义的变量。
lambda表达式内的代码是在repeatMessage执行返回后,变量遗失后很久才调用。为什么text仍然可以被访问?
为了理解这个原理,lambda表达式的三个组成部分
1.代码块
2.变量
3.自由变量的值。自由变量是未定义在lambda表达式内,且不是lambda参数的量。
我们的例子中,lambda只有一个自由变量text,lambda表达式的数据结构必须存储这个变量。这样的值会被Lambda表达式捕获。
lambda表示式可以捕获包围域中的变量。在Java中,为了确保被捕获的变量被很好的定义有一个重要的限制条件。在Lambda表达式中,引用变量不可以被改变。
举个例子,下述代码是非法的。
public static void countDown(int start,int delay){
ActionListener listener =event->
{
start--;//ERROR:变量不可被改变。
System.out.println(start);
};
}
lambda表达式外变量的改变也是非法的:
public static void repeatMessage(String text,int delay){
for(int i=1;i<=count;i++){
ActionListener listener =event->
{
System.out.println(i+": "+text);//Error:i是可变的。
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay ,listener).start();
}
}
规则是任何被捕获的变量必须是有效最终的(effectively final),这个有效最终值不可在被初始化后被赋予新值。在我们的例子中text始终指向同一个String对象。但i是可变的,i就不能被表达式捕获。
lambda表达式的身体和嵌套块拥有同样的作用域,所以会有命名冲突问题。
Path first=Path.of("/user/bin/");
Comparator<String> comp
=(first,second)->first.length()-second.length();//ERROR first already defined
6.2.7 处理Lambda表达式
下面书写运用表达式的方法。
使用Lambda表达式的关键是延期执行。如果你要立即执行代码,你不需要lambda表达式。延迟处理的情况有
1.代码运行在独立的线程上
2.代码需要运行多次
3.代码在算法的规定时间点运行
4.当某种事件发生时执行代码
5.只在必要时运行代码
最简单的例子,你需要重复执行一个行为n次
repeat(10,()->System.out.println("Hello,world!"));
为了接受一个lambda表达式,我们需要提供一个功能接口。比如使用Runnable接口
public static void repeat(int n,Runnable action){
for(int i=0;i<n;i++)action.run();
}
再举一个更复杂的例子。我们需要一个传递Int参数,并返回Void参数的功能接口。下述代码实现倒计时
public interface IntComsumer{
void accept(int value);
}
public static void repeat(int n,IntComsumer action){
for(int i=0;i<n;i++)action.accept(i);
}
repeat(10,i->System.out.println("countdown:"+(9-i)));
6.2.8 Comparator接口
静态comparing方法将类型T转换为课比较类型。举例:
Array.sort(people,Comparator.comparing(Person::getName));
你可以链接comparator使用thenComparing方法
例:如果LastName相等,比较FirstName
Arrays.sort(people,Comparator.comparing(Person::getLastName).thenComparing(Person::getFirstName);
使用名字长度比较排序
Arrays.sort(people,Comparator.comparing(Person::getName,(s,t)->Integer.compare(s.length(),t.length()))));
comparing,thenComparing方法避免使用封装的int,long,double你可以使用
Array.sort(people,Comparator.comparingInt(p->p.getName().length()));
如果你的比较函数会返回null,你要使用nullsFirst nullsLast适应器。这些静态方法将拿到一个Comparator并修改它,使它遇到Null时不会抛出异常。
而是按较小的或者较大的值设置。
nulsFirst方法要一个比较器,这样可以比较两个字符串。naturalOrder方法制作一个Comparator,用于任何实现了Comparable接口的类。
Comparator.
Array.sort(people,comparing(Person::getMiddleName,nullsFirst(naturalAOrder())));
静态reverseOrder方法可以颠倒自然排序,要颠倒任何比较器使用reversed实例方法。例如
naturalOrder().reversed()和reverseOrder()等价。
6.3内部类
内部类可以对同一包中的其他类实现隐藏。
内部类方法可以获得数据在它的定义域内,包括private数据。
任何声明在内部类中的静态变量都要声明为final并在编译时被初始化。如果域不是常数,那么它可能不是唯一的。
内部类不能有静态方法。
6.3.4本地内部类
本地类不能使用public private等权限修饰符。
本地类有一个优势,他们对外部世界完全隐藏。除过声明它的方法其他类都无法获取它。
本地类可以访问本地变量,但是变量必须是final的,赋值后不能改变。
6.3.7 静态内部类
将一个类隐藏在另一个类内部,且内部类没有外部类的引用,可以用static修饰内部类。
6.4Service Loaders(1卷p546)
定义一个接口,提供每个实例需要使用的方法。比如我们需要加解密的方法。
package serviceLoader;
public interface Cipher{
byte[] encrypt(byte[] source,byte[] key);
byte[] decrypt(byte[] source,byte[] kety);
int strength();
}
服务提供者提供多个类实现了这个服务。如:
package serviceLoader.impl;
public class CaesarCipher implements Cipher{
public byte[] encrypt(byte[] source,byte[] key){
var result=new byte[source.length];
for(int i=0;i<source.length;i++)
result[i]=(byte)(source[i]+key[0]);
return result;
}
public byte[] decrypt(byte[] source,byte[] key){
return encrypt(source,new byte[]{(byte)-key[0]};
}
public int strength(){return -1;}
}
接口类可以在任意包内,不用和服务接口在同一个包。但他们都必须要有一个没有参数的构造函数。
添加一个UTF-8编码的文本文件到META-INF/services 目录中,名字必须匹配接口名。在我们这个例子中
文件META-INF/services/serviceLoader.Cipher必须包含serviceLoader.impl.CaesarCipher
上述准备完成后:
程序可以初始化一个service Loader
public static serviceLoader<Cipher> cipherLoader=ServiceLoader.load(Cipher.class);
iterator方法返回service lodaer所有可用的实现。
public static Cipher getCipher(int minStrength){
for(Cipher cipher : cipherLoader)
if(ciphers.strength()>=minStrength)return cipher;
return null;
}
可选的,你可以用流定位需要的服务
public static Optional<Cipher> getCipher(int min Strength){
return cipherLoader.stream()
.filter(descr->descr.type()==serviceLoader.impl.CaesarCipher.class)
.findFirst()
.map(ServiceLoader.Provider::get);
}
Optional<Cipher> cipher=cipherLoader().findFirst();
6.5代理Proxy
1.调用句柄,调用句柄是任何实现了InvocartionHandeler接口的对象。这个接口只有一个方法:
Object invoke(Object proxy,Method method,Object[] args)
当代理对象的一个方法被调用时,代理对象的invoke方法被调用,伴随着Method对象,和原始参数列表被调用。调用句柄必须弄清如何处理调用。
6.5.2创建代理对象
为了创建代理对象,使用Poxy类中的newProxyInstance方法。它有三个参数:
1.类加载器
2.Class对象数组
3.调用句柄
有两个遗留问题,如何定义句柄,代理对象能干什么。代理可以:
1.路由远端服务器的方法调用
2.运行时支持用户接口事件的相应
3.排错时跟踪方法调用。
示例程序我们利用代理和调用句柄来跟踪方法调用。
class TraceHandler implements InvocationHandler{
private Object target;
public TraceHandler(Object t){
target =t;
}
public Object invoke(Object proxy ,Method m,Object[] args) throws Throwable{
...//print method name and parameters
return m.invoke(target,args);
}
下例构造proxy对象,当任意方法调用时跟踪
Object value=...;
var handler=new TraceHandler(value);
var interfaces=new Class[]{Comparable.class};
Object proxy=Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class},handler);
6.5.3 代理类属性
所有的代理类都继承于Proxy类,一个Proxy类只能有一个实例,the invocation handler,它定义在Proxy的超类中。任何额外数据需要在代理对象中执行的必须存储在invocation handler中。
所有的代理类都覆盖了Object 类的toString equals hashCode方法。像是所有其他的代理方法一样。它会调用invoke invocation handler。其他Object类的方法,如clone,getClass没有被定义。
所有代理类的名字没有被定义,虚拟机中类的名字以$Proxy开头。
如果你调用了newProxyInstance方法两次,使用相同的class loader和接口数组,你会得到两个对象。你可以通过getProxyClass方法获得该类。
Class proxyClass=Proxy.getProxyClass(null,interfaces);
代理类总是public final的。如果代理类实现的所有接口都是public的,代理类就不属于任何其他的包。换而言之,所有非public接口必须属于相同的包,这个代理类也必须属于同一个包。

浙公网安备 33010602011771号