【Java】 第六章 接口、lambda 表达式与内部类 Java核心技术卷1基础知识原书第10版 读书笔记

6. 接口、lambda 表达式与内部类

6.1 接口

接口主要用来描述类具有什么功能,而并不给出每个功能的具体实现。

public interface Comparable
{
    int compareTo(Object other) ;
}
6.1.1 接口概念

一个类只能有一个超类,但可以实现一个或多个接口。

接口绝不能含有实例域, 在JavaSE 8 之前, 也不能在接口中实现方法。现在已经可以在接口中提供简单方法了。当然, 这些方法不能引用实例域。

提供实例域和方法实现的任务应该由实现接口的那个类来完成。因此, 可以将接口看成是没有实例域的抽象类,但是这两个概念还是有一定区别的。

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

  1. 将类声明为实现给定的接口。
  2. 对接口中的所有方法进行定义。
class Employee implements Cloneable, Comparable;//将类声明为实现某个接口
6.1.2 接口的特性

接口不是类,不能用new实例化一个接口。

尽管不能构造接口的对象,却能声明接口的变量,接口变量必须引用实现了接口的类对象:

Comparable x; // OK
x = new Employee(. . .); // OK

检查一个对象是否实现了某个特定的接口

if (anObject instanceof Comparable) { . . . }

接口的扩展

public interface Moveable
{
    void move(double x, double y) ;
}

public interface Powered extends Moveable
{
    double milesPerCallon();
}

虽然在接口中不能包含实例域或静态方法,但却可以包含常量。

与接口中的方法都自动地被设置为public—样,接口中的域将被自动设为public static final。

6.1.3 接口与抽象类

为什么要用接口而不用抽象类呢?使用抽象类表示通用属性存在这样一个问题: 每个类只能扩展于一个类。但每个类可以实现多个接口。

6.1.4 静态方法

在Java SE 8 中,允许在接口中增加静态方法。只是这有违于将接口作为抽象规范的初衷。

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

6.1.5 默认方法

default

默认方法可以调用任何其他方法。

接口演化

6.1.6 解决默认方法冲突

如果先在一个接口中将一个方法定义为默认方法, 然后又在超类或另一个接口中定义了同样的方法,采取规则:

  1. 超类优先
  2. 接口冲突

6.2 接口示例

6.2.1 接口与回调

指出某个特定事件发生时应该采取的动作

定时器和动作监听器的使用

6.2.2 Comparator接口

按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。要处理这种情况,ArrayS.Sort 方法还有第二个版本, 有一个数组和一个比较器( comparator )作为参数, 比较器是实现了Comparator 接口的类的实例。

public interface Comparators
{
    int compare(T first, T second);
}
class LengthComparator implements Comparator<String>
{
    public int compare(String first, String second) {
        return first.length() - second.length()}
}

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

Cloneable 接口是Java 提供的一组标记接口( tagging interface ) 之一。标记接口不包含任何方法; 它唯一的作用就是允许在类型查询中使用instanceof。

浅拷贝深拷贝

所有数组类型都有一个public 的clone 方法, 而不是protected: 可以用这个方法建立一个新数组, 包含原数组所有元素的副本:

int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
int[] cloned = luckyNumbers.clone();
cloned[5] = 12; // doesn't change luckyNumbers[5]

6.3 lambda 表达式

6.3.1 为什么引入lambda 表达式

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

6.3.2 lambda 表达式的语法

有三个部分:

  1. 一个代码块
  2. 参数
  3. 自由变量的值,这里指非参数而且不在代码中定义的变量
(String first, String second)
-> first.length() - second.length()

如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在{}中,并包含显式的return 语句

(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 >= 0;i--) System.out.println(i); }

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

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

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

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

无需指定lambda 表达式的返回类型。lambda 表达式的返回类型总是会由上下文推导得出。

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

(int x)-> { if (x >= 0) return 1; }//不合法
6.3.3 函数式接口

对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个lambda 表达式。这种接口称为函数式接口( functional interface )。

Arrays.sort 方法的第二个参数需要一个Comparator 实例, Comparator 就是只有一个方法的接口, 所以可以提供一个lambda 表达式:

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

lambda 表达式可以转换为接口

实际上,在Java 中, 对lambda 表达式所能做的也只是能转换为函数式接口。甚至不能把丨ambda 表达式赋给类型为Object 的变量,Object 不是一个函数式接口。

  • Predicate接口
public interface Predicate<T>
{
    boolean test(T t);
    // Additional default and static methods
}

ArrayList 类有一个removelf 方法, 它的参数就是一个Predicate。这个接口专门用来传递lambda 表达式。例如,下面的语句将从一个数组列表删除所有null 值:

list.removelf(e -> e == null);
  • Supplier < T >接口
6.3.4 方法引用
Timer t = new Timer(1000, event -> System.out.println(event)):

直接把println 方法传递到Timer 构造器

Timer t = new Timer(1000, System.out::println);

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

类似的,Math::pow 等价于(x,y) ->Math.pow(x, y)

String::compareToIgnoreCase等同于(x, y) -> x.compareToIgnoreCase(y)

Arrays.sort(strings,String::compareToIgnoreCase);//对字符串排序而不考虑字母的大小写

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

  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod

注意,只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。

s -> s.length() == 0这里有一个方法调用,但是还有一个比较,所以不能用方法引用。

可以在方法引用中使用thissuperthis::equals 等同于x -> this.equals(x)

6 .3.5 构造器引用

构造器引用与方法引用很类似,只不过方法名为new。例如,Person::newPerson构造器的一个引用。

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

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

6.3.6 变量作用域

在Java 中, lambda 表达式就是闭包。

lambda 表达式可以捕获外围作用域中变量的值。在Java 中, 要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda 表达式中, 只能引用值不会改变的变量。因为如果在lambda 表达式中改变变量, 并发执行多个动作时就会不安全。如果在lambda 表达式中引用变量, 而这个变量可能在外部改变,这也是不合法的。

也就是:lambda 表达式中捕获的变量必须实际上是最终变量( effectivelyfinal),这个变量初始化之后就不会再为它赋新的值。

表达式的体与嵌套块有相同的作用域。lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

在一个lambda 表达式中使用this 关键字时, 是指创建这个lambda 表达式的方法的this参数。

public class ApplicationO
{
    public void init ()
    {
        ActionListener listener * event ->
        {
        System.out.print n(thi s.toStringO) ;
        }
        ...
    }
}

表达式this.toStringO 会调用Application 对象的toString 方法,而不是ActionListener 实例的方法。

6.3.7 处理lambda表达式

使用lambda 表达式的重点是延迟执行(deferred execution )

例子:重复一个动作n次

repeat(10, 0 -> System.out.println("Hello, World!"))public static void repeat(int n, Runnable action)
{
	for (int i = 0; i < n; i++) action.run() ;
}

改进:告诉这个动作它出现在哪一次迭代中

public interface IntConsumer
{
	void accept(int value) ;
}
public static void repeat(int n, IntConsumer action)
{
	for (int i = 0; i < n; i++) action.accept(i);
}
repeat(10, i -> System.out.println("Countdown: " + (9 - i)));
6.3.8 再谈Comparator

Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda 表达式或方法引用。

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

Arrays.sort(people, Comparator.comparing(Person::getName)) ;//按名字排序

Arrays.sort(people,
	Comparator.comparing(Person::getlastName
	.thenConipari ng(Pe rson::getFi rstName)) ;//如果姓相同,就用第二个比较器

Arrays.sort(people, Comparator.companng(Person::getName,
	(s, t) -> Integer.compare(s.1ength() , t.length())))//根据名字的长度排序

6.4 内部类

内部类( inner class ) 是定义在另一个类中的类。

用内部类的原因:

  1. 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
  2. 内部类可以对同一个包中的其他类隐藏起来。
  3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较便捷。不过如今lambda表达式在这方面可以做的更好。
6.4.1 使用内部类访问对象状态

内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。为此,内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象。

只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。

6.4.2 内部类的特殊语法规则

使用外围类引用的正规语法:

OuterClass.this

在外围类的作用域之外,可以这样引用内部类:

OuterClass.InnerClass

内部类中声明的所有静态域都必须是final。

内部类不能有static 方法。

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

内部类是一种编译器现象, 与虚拟机无关。

6.4.4 局部内部类

局部类不能用public 或private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。

局部类有一个优势, 即对外部世界可以完全地隐藏起来。

6.4.5 由外部方法访问变量

与其他内部类相比较, 局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过, 那些局部变量必须事实上为final。这说明, 它们一旦赋值就绝不会改变。

在内部类被首次提出时, 原型编译器对内部类中修改的局部变量自动地进行转换。不过, 后来这种做法被废弃。毕竟, 这里存在一个危险。同时在多个线程中执行内部类中的代码时, 这种并发更新会导致竞态条件(第14章)

6.4.6 匿名内部类

假如只创建这个类的一个对象,甚至不需要为类指定名字,这样的类称为匿名内部类(anonymous inner class)。

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

// 例如:
public void start(int interval, boolean beep)
{
    ActionListener listener = new ActionListener()
    {
        public void actionPerformed(ActionEvent event)
        {
            System.out.println("At the tone, the time is " + new Date())if (beep) Toolkit.getDefaultToolkit().beep();
        }
    };
    Timer t = new Timer(interval, listener);
    t.start()}

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

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

  • 构造一个类的新对象与构造一个扩展了那个类的匿名内部类的对象之间有什么差别?

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

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

双括号初始化:外层括号建立了ArrayList 的一个匿名子类。内层括号则是一个对象构造块:

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

生成曰志或调试消息时,用匿名类:

System.err.println("Something awful happened in " + getClass())//但是对静态方法不奏效,因为调用getClass 时调用的是this.getClass(), 而静态方法没有this
//这时候可以用下面的表达式
new Object0{}.getCIass0-getEndosingClass() // gets class of static method

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

6.4.7 静态内部类

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

静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所冇内部类完全一样。

与常规内部类不同, 静态内部类可以有静态域和方法。

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值