Java核心技术-接口、lambda表达式与内部类

时间:2022-09-25 15:14:27

本章将主要介绍:

接口技术:主要用来描述类具有什么功能,而并不给出每个功能的具体实现。一个类可以实现一个或多个接口。

lambda表达式:这是一种表示可以在将来的某个时间点执行的代码块的简洁方法。

内部类机制:内部类定义在另一个类的内部,其中的方法可以访问包含它们的外部类的域。

代理:一种实现任意接口的对象。

1 接口

1.1 接口的概念

概念:接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。

“如果类遵从某个特定的接口,那么就履行这项服务”:

Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。

接口中的所有方法自动地属于public。因此,在接口中声明方法时,不必提供关键字public。

接口中可以定义常量,但绝不能含有实例域,Java SE 8之后,可以在接口中提供简单的方法(不能引用实例域)

提供实例域和方法实现的任务应该由实现接口的类完成,可以将接口看成是没有实例域的抽象类。

为了让类实现一个接口,需要一下步骤:

1.将类声明为实现给定的接口(implements关键字)。

2.对接口中的所有方法进行定义(实现接口时,必须把方法声明为public)。

不直接提供compareTo方法而实现接口的原因:

Java是一种强类型语言,在调用方法的时候,编译器将检查这个方法是否存在。而实现了Compareble接口的类必定存在这个方法。

1.2 接口的特性

接口不是类,所以不能用new实例化一个接口,然而,却能声明接口的变量,接口变量必须引用实现了接口的类对象。

使用instanceof检查一个对象是否属于某个特定的类,使用isInstance检查一个对象是否实现了某个特定的接口

接口中的常量被自动设为public static final。

尽管每个类只能够拥有一个超类,但却可以实现多个接口。这就为定义类的行为提供了极大的灵活性。(使用逗号将实现的每个接口分开)

1.3 接口与抽象类

接口与抽象类的区别:

使用抽象类表示通用属性存在每个类只能扩展与一个类的问题,但每个类可以实现多个接口。

1.4 静态方法

在Java SE 8中,允许在接口中增加静态方法。

目前为止,通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和使用工具类,如Collection/Collections或Path/Paths。

这个伴随类实现了相应接口的部分或全部方法。

现在只要在实现接口时,加入静态方法,就不再需要再为使用工具方法另外提供一个伴随类。

1.5 默认方法

可以为接口方法提供一个默认实现。必须用default修饰符标记这样的一个方法。

实现这个接口的类可以选择性的覆盖接口中的默认方法。

默认方法的一个重要用法是——接口演化(给原先设计的接口添加默认方法,不需要重新编译之前实现接口的所有类且这些类调用新添加的方法时将调用接口中的实现)

1.6 解决默认方法冲突

考虑这样的情况:如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法,会发生什么情况?(二义性)

Java的处理规则如下:

1.超类优先(考虑到接口新增默认方法的兼容性)

2.接口冲突:考虑三种情况

1.两个接口中都有这个方法的默认实现——必须覆盖这个方法来解决冲突(可以选择两个冲突方法中任意一个提供的默认实现)

2.两个接口中有一个有这个方法的默认实现——必须覆盖这个方法来解决冲突(可以选择两个冲突方法中提供默认实现的那一个)

3.两个接口中都没有这个方法的默认实现——可以实现这个方法,也可以不实现这个方法。如果不实现,这个类本身就是抽象的


2 接口示例

2.1 接口与回调

回调是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。

ActionListener listen=new TimePrinter();
Timer time=new Timer(10000,listen)

2.2 Comparator接口

考虑这样的情况:

一个类本身已经继承Comparable接口,有一个按字典顺序排序的comparaTo方法。现在我们希望按长度递增的顺序对这个类进行排序。而不是按字典排序。

解决这种情况,考虑使用一个数组和一个比较器作为参数,比较器是实现了Comparator接口的类的实例。

Comparator<String> comp=new LengthComparator();
if(comp.compare(words[i],words[j])>0...

将这个调用与words[i].compareTo(words[j])做比较。这个compare方法要在比较器对象上调用,而不是在字符串本身上调用。

Comparator与Comparable的区别:

1.使用Comparator进行比较时需要先建立一个Comparator具体实例

2.Comparator调用对象是比较器,comparable调用对象是字符串本身

3.利用Comparator可以实现更多样的比较方式

2.3 对象克隆

本节讨论Cloneable接口,这个接口指示一个类提供了一个安全的clone方法

考虑这样的情况:

当我们为一个包含对象引用的变量建立副本时,原变量和副本都是同一个对象的引用。这说明任何一个变量的改变都会影响另一个变量。

Employee origina=new Employee("john Public",50000);
Employee copy=original;
copy.raiseSalary(10);

如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用clone方法。

Employee copy=original.clone();
copy.raiseSalary(10);

默认的克隆操作是浅拷贝,并没有克隆对象中引用的其他对象。

如果原对象中的所有子对象都是不可变的,那么使用浅拷贝是安全的。

如果子对象属于一个可变类,就需要使用深拷贝。

对于每一个类,需要确定:

1.默认的clone方法是否满足要求;

2.是否可以在可变的子对象上调用clone来修补默认的clone方法;

如果选择第一项或第二项,类必须:

1.实现Cloneable接口;

2.重新定义clone方法,并指定public访问修饰符。

Object中的clone方法声明为protected,因此要在实现类中重新定义为public,才能在其他类中调用实例化的这个实现类对象的clone方法。

Cloneable接口是标记接口,标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof

如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会抛出一个CloneNotSupportedException。

所有数组类型都有一个public的clone方法


3 lambda表达式

采用一种简洁的语法定义代码块

3.1 为什么引入lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。

比如传递一个实现ActionListener接口的类实例到定时器或实现Comparator接口的类实例到sort方法。

3.2 lambda表达式的语法

(String first,String second)
->first.length()-second.length()

这就是你看到的第一个lambda表达式。lambda表达式就是一个代码块,以及必须传入代码的变量规范。

即使lambda表达式没有参数,仍然要提供空括号

()->{for(int i=100;i>=0;i--)System.out.println(i);}

如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型

Comparator<String> comp
=(first,second)
->first.length()-second.length();

如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

ActionListener listener=event->
System.out.println("The time is"+new Date());

无需指定lambda表达式的返回类型

如果lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。

(int x)->{if(x>=0)return 1;}

3.3 函数式接口

概念:只有一个抽象方法的接口称为函数式接口

可以用lambda表达式替换函数式接口

例如:Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式

Arrays.sort(words,
(first,second)->first.length()-second.length());

在底层,Arrays.sort方法会接收实现了Comparator<String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。(要接受lambda表达式可以传递到函数式接口)

最好把lambda表达式看作是一个函数,而不是一个对象。

实际上,在Java中,对lambda表达式能做的也只是能转换为函数式接口。

3.4 方法引用

要用::操作符分隔方法名与对象或类名,主要有三种情况:

*object::instanceMethod

*Class::staticMethod

*Class::instanceMethod

前两种情况,方法引用等价于提供方法参数的lambda表达式:

表达式System.out::println是一个方法引用,它等价于lambda表达式x->System.out.println(x)

表达式Math::pow是一个方法引用,它等价于lambda表达式(x,y)->Math.pow(x,y)

对于第三种情况,第一个参数会成为方法的目标:

表达式String:;compareToIgnoreCase是一个方法引用,它等价于(x,y)->x.compareToIgnoreCase(y)

类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例

可以在方法引用中使用this和super参数

this::equals等同于x->this,equals(x)

3.5 构造器引用

构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。具体调用哪一个构造器取决于上下文。

可以用数组类型建立构造器引用,例如,int[]::new是一个构造器引用,等价于lambda表达式x->new int[x]

Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。

3.6 变量作用域

通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。

例子:

public static void repeatMessage(String text,int delay)
{
ActionListener listener=event->
{
System.out.priintln(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}

lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

lambda表达式可以捕获外围作用域中变量的值,具体细节如下:

可以把lambda表达式转换为包含一个方法的对象,这样*变量的值就会复制到这个对象的实例变量中

一个lambda表达式由下面三部分组成:

1.一个代码块;

2.参数;

3.*变量的值,这是指非参数而且不在代码中定义的变量。

关于代码块以及*变量值有一个术语:闭包;在Java中,lambda表达式就是闭包。

在lambda表达式中,只能引用值不会改变的变量(既不能在外部改变,也不能在内部改变)

lambda表达式中捕获的变量必须实际上是最终变量(如String对象)

lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。

lambda表达式中的this指的是拥有lambda表达式的类的对象。

3.7 处理lambda表达式

适用lambda表达式的重点是延迟执行。

延迟的原因如下:

*在一个单独的线程中运行代码

*多次运行代码

*在算法的适当位置运行代码(例如,排序中的比较操作)

*发生某种情况时执行代码(点击一个按钮)

*只有必要时才运行代码

要接受lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。

Java核心技术-接口、lambda表达式与内部类

Java核心技术-接口、lambda表达式与内部类

使用表6-2这些特殊化规范来减少自动装箱。

可以使用@FunctionalInterface注解一个函数式接口

3.8 再谈Comparator

Comparator接口包含很多方便的静态方法来创建比较器。

静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String),对要比较的对象应用这个函数,然后对返回的键完成比较。

例如:Arrays.sort(people,Comparator.comparing(Person::getName));

可以把比较器与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())));

为避免int、long或double值的装箱:

Arrays.sort(people,Comparator.comparingInt(p->p.getName().length()));

如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器(nullsFirst方法需要一个比较器):

Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(naturalOrder()));

4 内部类

内部类是定义在另一个类中的类。

使用内部类的主要原因有以下三点:

*内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。

*内部类可以对同一个包中的其他类隐藏起来。

*当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷。

4.1 使用内部类访问对象状态

内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。

内部类的对象总有一个隐式引用,它指向了创建它的外部类对象。

这个引用在内部类的定义中是不可见的。编译器修改了所有内部类的构造器,在其中添加了一个外围类引用的参数,当在外围类中构造内部类实例的时候,编译器就会将this引用传递给内部类的构造器。

4.2 内部类的特殊语法规则

OuterClass.this

outerObject.new InnerClass(contruction parameters)

OuterClass.InnerClass

内部类中声明的所有静态域都必须是final,确保类域唯一。

内部类的static方法只能访问外围类的静态域和方法。

4.3 内部类是否有用、必要和安全

编译器会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,而虚拟机对此一无所知。

例如:TalkingClock$TimePrinter

由于内部类拥有访问特权,所以与常规类比较起来功能更加强大。

如果内部类访问了外围类的私有数据域,就有可能通过附加在外围类所在包中的其他类访问它们,但做这些事情需要高超的技巧和极大的决心。

4.4 局部内部类

在一个方法中定义的类称为局部内部类,它不能用public或private访问说明符进行声明,作用域被限定在声明这个局部类的块中。

局部内部类的优势是可以对外部世界完全地隐藏起来,除了它所在的方法之外,没有任何方法知道这个局部内部类的存在。

4.5 由外部方法访问局部类

方法参数(必须为final)会保存到局部类中(通过局部类的构造器)

与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。但这些局部变量必须是final的。

4.6 匿名内部类

如果只创建一个局部内部类的对象而不命名。这种类被称为匿名内部类。

通常的语法格式为:

new SuperType(construction parameters)
{
inner class methods and data
}

其中SuperType可以是ActionListener这样的接口,于是内部类就要实现这个接口,SuperType也可以是一个类,于是内部类就要扩展它。

由于构造器的名字必须与类名相同,而匿名类没有类名,所以不能有构造器,于是将构造器参数传给超类。尤其是在内部类实现接口时,不能有任何构造参数。

new InterfaceType()
{
methods and data
}

如果构造参数的闭小括号后面跟了一个开大括号,正在定义的就是匿名内部类。

多年来,Java程序员习惯的做法是用匿名类实现事件监听器和其他回调,如今最好使用lambda表达式。

奇淫技巧:

1.“双括号初始化”:

如果只在一个地方需要构造一个数组列表,后面将不再需要,可以构造一个匿名列表。

invite(new ArrayList<String>(){{add("Harry");add("Tony")});

注意这里的双括号,外层括号建立了ArrayList的一个匿名子类。内层括号则是一个对象构造块;

2.想要在生成日志时包含当前类名(getClass()),不过,这对于静态方法不奏效。毕竟,调用getClass时调用的是this.getClass(),而静态方法没有this。

所以应该使用下面的表达式:

new Object(){}.getclass().getEnclosingClass()

在这里,new Object(){}会建立Object的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类,也就是包含这个静态方法的类。

注意,对匿名子类做equals操作(if(getClass!=other.getClass())return false)会失败。

4.7 静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。

考虑到类名的冲突,可以使用内部类,只有内部类可以声明为static,静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。

声明在接口中的内部类自动成为static和public类


5 代理

利用代理可以在运行时创建一个实现了一组给定接口的新类。

5.1 何时使用代理

假设有一个表示接口的Class对象,它的确切类型在编译时无法知道,要想构造一个实现这些接口的类,需要在程序处于运行状态时定义一个新类。

代理类可以在运行时创建全新的类,这样的代理类能够实现指定的接口,并且它具有以下的方法:

*指定接口所需要的全部方法

*Object类中的全部方法

不能在运行时定义这些方法的新代码,要提供一个调用处理器(实现了InvocationHandler接口),它只有一个方法

Object invoke(Object proxy,Method method,Object[] args)

无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。

使用代理原因:

*路由对远程服务器的方法调用

*在程序运行期间,将用户接口事件与动作关联起来

*为调试,跟踪方法调用

5.2 创建代理对象

要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:

*一个类加载器(用null表示默认加载器)

*一个Class对象数组,每个元素都是需要实现的接口

*一个调用处理器

Object value=...;
InvocationHandler=new TraceHandler(value);
Class[] interfaces=new Class[]{Compareble.class}
Object proxy=Proxy.newProxyInstance(null,interfaces,handler);

5.3 代理类的特性

代理类是在程序运行过程中创建的,然而,一旦被创建,就变成了常规类。

所有代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,所需要的任何附加数据都必须存储在调用处理器中。

所有的代理类都覆盖了Object类中的方法toString、equals和hashCode。如同所有的代理方法(例如代理的Comparator接口)一样,这些方法仅仅调用了调用处理器的invoke。

Object类中的其它方法(如clone和getClass)没有被重新定义。

代理类一定是public和final。对于特定的类加载器和预设的一组接口来说,只能有一个代理类。

可以通过调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类。