《Java核心技术》学习笔记——第6章 接口、lambda表达式与内部类

版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。

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

作者:ExcelMann,转载需注明。

第6章内容目录:

  • 接口
  • lambda表达式
  • 内部类
  • 服务加载器
  • 代理

一、接口

1、接口的概念

  1. 接口不是类,而是对希望符合这个接口的类的一组需求。
    比如,Arrays类中的sort方法承诺可以对对象数组进行排序,但是要求对象所属的类必须实现Comparable接口(这样,该对象所属的类保证有接口中的compareTo方法,保证sort方法可以调用该compareTo方法);
  2. 接口中的所有方法都自动是public方法,因此在接口中声明方法时,不必提供关键字public;
    接口中的字段总是public static final
  3. 接口绝不会有实例字段;也不会实现方法(但是会有默认方法);
  4. 类实现一个接口,需要完成下面两个步骤:
    1)将类声明为实现给定的接口;
    2)对接口中的所有方法提供定义;
  5. compareTo方法在继承中会出现问题:当Manager继承了Employee,而Employee实现的是Comparable<Employee>,而不是Comparable<Manager>。当执行以下代码时会出现异常。
    解决方案(类似于equals):
    1)如果不同子类中的比较有不同的含义,就应该将属于不同类的对象之间的比较规定为非法;
    2)如果存在一个能够比较子类对象的通用算法,那么可以在超类中提供一个compareTo方法,并将这个方法声明为final;
class Manager extends Employee{
	public int compareTo(Employee e){
		Manager manager = (Manager)e; //会出现ClassCastException
	}
}

2、接口的属性

  1. 虽然不能实例化接口,但是可以声明接口的变量,引用实现了这个接口的类对象:
    Comparable x = new Employee();
    接下来可以使用instanceof检查一个对象是否实现了某个特定的接口;
    if(anObject instanceof Comparable){…}

3、静态和私有方法

  1. 在Java8中,允许在接口中增加静态方法。(有违于将接口作为抽象规范的初衷)
  2. 在Java9中,接口中的方法可以是private;

4、默认方法

  1. 可以为接口方法提供一个默认实现(实现该接口的类可以不用定义该方法)。必须用default修饰符标记这样一个方法;
public interface Comparable<T>{
	default int compareTo(T other){return 0;}
}
  1. 默认方法的用法(好处)
    1)例如,对于Iterator接口,用于访问数据结构中的元素,这个接口中声明了一个默认的remove方法,只有当可写的迭代器实现该接口时,才需要定义该方法,而对于只读的迭代器实现该接口时,可以不用操心实现该方法;
    default void remove(){…}
    2)默认方法可以调用其他方法。例如,Collection接口定义一个便利方法:
    3)“接口演化”的用法。以Collection接口为例,这个接口作为Java的一部分已经有很多年了。假设以前提供了这样一个类:
    public class Bag implements Collection
    后来,在Java8中,又为该接口增加了一个stream方法,如果该方法是默认的,那么Bag类可以正常编译,另外如果重新编译而加载这个类,并在一个Bag实例上调用stream方法,将调用Collection.stream方法;
2)
public interface Collection{
	int size(); //public的抽象方法,由实现类定义
	default boolean isEmpty(){return size()==0;}
	//这样实现Collection的程序员就不用担心实现isEmpty方法了;
}

5、解决默认方法冲突

如果在接口中将一个方法声明为默认方法,并且与继承的超类或另一个接口中定义的方法相同,会发生什么情况?

  1. 超类有限(类优先原则):如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略;
  2. 接口冲突:不论是两个接口都默认实现了同个签名的方法,还是至少有一个接口默认实现了,编译器都会报告错误,程序员就必须解决这个二义性;
class Student implements Person,Named{
	public String getName(){return Person.super.getName();}
}

6、接口与回调

这节将通过接口和回调,实现定时器和动作监听器的配合。

  1. 回调:在这种模式中,可以指定某个特定事件发生时应该采取的动作;
  2. 定时器:Timer构造器的第一个参数是一个时间间隔(单位是毫秒),即经过多长时间通知一次。第二个参数是监听器对象
  3. 如何告诉告诉定时器要做什么?在Java中,通过向定时器传入某个类的对象,然后,定时器调用这个对象的方法;
  4. 定时器要求传递的对象所属的类实现了java.awt.event包的ActionListener接口。当到达指定的时间间隔时,定时器会调用监听器的actionPerformed方法;
public interface ActionListener{
	void actionPerformed(ActionEvent event);
	//注意ActionEvent参数,这个参数提供了事件的相关信息,例如,发生这个事件的时间;
}

7、Comparator接口

前面了解到可以通过实现Comparable接口,定义该接口的compareTo方法,从而与Arrays.sort搭配实现对象数组的排序。
另外,Array.sort方法还有第二个版本,用一个数组和比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。

public interface Comparator<T>{
	int compare(T first,T second);
}

//构造一个比较器
class LengthComparator implements Comparator<String>{
	public int compare(String first,String second){
		return first.length()-second.length();
	}
}

//对数组进行排序
String[] friends={...};
Arrays.sort(friends, new LengthComparator());

8、对象克隆

  1. 浅拷贝:Object类的clone方法克隆一个对象时,并没有克隆对象中引用的其他对象;
    影响:当原对象和浅克隆对象共享的子对象是不可变的,那么这种共享是安全的。或者,在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况也是安全的;
  2. 深拷贝:可以通过重新定义clone方法建立深拷贝,即同时克隆所有子对象(若是不可变的,那么这种共享算安全);
class Employee implements Cloneable{
	public Employee clone() throws CloneNotSupportedException{
		Employee cloned = (Employee) super.clone();

		cloned.hireDay = (Date)hireDay.clone();	//同时克隆子对象

		return cloned;
	}
}
  1. 标记接口:标记接口不包含任何方法,它唯一的作用就是允许在类型查询中使用instanceof;
  2. Cloneable接口就是标记接口。它没有指定clone方法,这个方法是从Object类继承的。不过,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常;
  3. 重新定义clone方法:当默认的clone方法不满足要求,或者你希望实现深拷贝,亦或是希望在其他类也能克隆自身类对象,那么可以在自身类中重新定义clone方法(为public);
class Employee implements Cloneable{
	public Employee clone() throws CloneNotSupportedException{
		return (Employee) super.clone();
	}
}
  1. 必须当心子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象;
    注意:子类覆盖父类的clone方法,方法中调用super.clone返回的是子类类型的对象;
  2. 所有数组类型都有一个公共的clone方法,而不是受保护的!

二、lambda表达式

1、为什么引入lambda表达式

在Java中,想要传递代码段,必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。

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

2、lambda表达式的语法

  1. lambda表达式,就是一个代码块,以及必须传入代码的变量规范。例子如下:
(String first, String second) //方法参数
-> first.length()-second.length()	//方法执行代码,可用{代码块}
  1. 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
()->{for(int i=100;i>=0;i--) System.out.println(...);}
  1. 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:
//这里将lambda表达式赋值给一个字符串比较器(称作函数式接口)
Comparator<String> comp
= (first,second)
-> first.length()-second.length();
  1. 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么还可以省略小括号:
ActionListener listener = event-> //event省略小括号
	System.out.println(...);
  1. 无须指定lambda表达式的返回类型。它总是会由上下文推到得出。例如下面的表达式,可以在需要int类型结果的上下文中使用:
(String first, String second) //方法参数
-> first.length()-second.length()
  1. 如果一个lambda表达式只在某些分支返回一些值,而另外一些分支不返回值,这是不合法的。例如:(int x)->{if(x>=0) return 1;}不合法;

3、(★)函数式接口

  1. 函数式接口:对于只有一个抽象方法的接口,当需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口;
    如:
//这里的(first,second)->first.length()-second.length(),代替了一个Comparator接口对象
Arrays.sort(planets, (first,second)->first.length()-second.length());
  1. 解释:在上面的例子中,实质上在底层,Arrays.sort方法的第二个参数会接收实现了Comparator<String>的某个类的对象。而在这个对象上调用compare方法(也就是函数式接口的唯一抽象方法)时会反过来执行lambda表达式的体
  2. 所以说,lambda表达式可以转换为函数式接口;
  3. 下面介绍两个比较有用的函数式接口:
    1)java.util.function包中的Predicate接口(例子中用于ArraiyList类的removeIf方法):
    2)供应者Supplier<T>接口。该接口没有参数调用的时候会生成一个T类型的值,主要用于实现懒计算
1public interface Predicate<T>
{
	boolean test(T t); //唯一的抽象方法
	...
}
//removeIf方法的参数就是Predicate,该接口专门用来传递lambda表达式
list.removeIf(e -> e == null); //该语句将从一个数组列表删除所有null值

2public interface Supplier<T>
{
	T get();
}
//下面这个不是最优的,因为我们预计day很少为null,所以希望在必要时才构造默认的LocalDate对象(实现懒计算)
//requireNonNullOrElseGet方法只有在需要值时才调用供应者
//LocalDate hireDay = Objects.requireNonNullOrElseGet(day, new LocalDate(1970,1,1));
LocalDate hireDay = Objects.requireNonNullOrElseGet(day, ()->new LocalDate(1970,1,1));

4、方法引用

  1. 方法引用:例如表达式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法;
    例如:
//lambda表达式的写法
var timer = new Timer(1000, event -> System.out.println(event));
//方法引用的写法
//这里System.out是对象,println是调用的方法,参数为函数式接口唯一抽象方法的参数
var timer = new Timer(1000, System.out::println);
//这里会生成一个ActionListener实例,它的actionPerformed(ActionEvent e)方法要调用System.out.println(e);
  1. lambda表达式与方法引用的区别:1)方法引用类似于lambda表达式,不是一个对象。但是,为一个类型为函数式接口的变量赋值时会生成一个对象;2)考虑一个方法引用,如separator::equals,如果separator为null,构造方法引用的时候会立即抛出空指针异常,而对于lambda表达式x-> separator.equals(x),只有在调用时才抛出空指针异常;
  2. (★)方法引用的几种类型
    1)object::instanceMethod:例如,对于System.out::println,对象是System.out,所以方法表达式等价于x->System.out.println(x),其中x为函数式接口方法的参数;
    2)Class::instanceMethod:这种情况下,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y);
    3)Class::staticMethod:这种情况下,所有参数都传递到静态方法作为参数。例如,Math::pow等价于(x,y)->Math.pow(x,y);
    4)Class::new:构造器引用,lambda参数传到这个构造器。例如,Integer::new,相当于x-> new Integer(x);
    5)Class[]::new:数组构造器引用,lambda参数是数组长度。例如,Integer[]::new,相当于n->new Integer[n];
  3. (★)注意:只有当lambda表达式的体只调用一个方法而不做其他操作时,才能将lambda表达式重写为方法引用!!
  4. 可以在方法引用中使用this参数和super参数。例如,this::equals等同于x->this.equals(x),或者super::greet等同于x->super.greet(x);

5、构造器引用

  1. Class::new:构造器引用,lambda参数传到这个构造器。例如,Integer::new,相当于x-> new Integer(x)。至于调用哪一个构造器,取决于参数;

6、变量作用域

  1. lambda表达式的3个部分:参数、一个代码块、自由变量的值(这里指非参数而且不在代码中定义的变量);
  2. lambda表达式可以捕获外围作用域变量的值。
    例如:
public static void repeatMessage(String text){
	ActionListener listener = event->
		{
			System.out.println(text); //text就是外围作用域变量
			...
		}
}
  1. 注意,lambda表达式中捕获的变量必须实际上是事实最终变量(即不会改变值的变量):
    1)在lambda中,只能引用值不会改变的变量。因为如果在lambda表达式中更改变量,并发执行多个动作时就会不安全;
    2)如果引用一个变量,而这个变量可能在外部改变,这也是不合法的;
  2. lambda表达式的体与嵌套块有相同的作用域;
  3. 在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:
public class Application{
	ActionListener listener = event ->{
		System.out.println(this.toString());//这里会调用Application对象的toString方法
	}
}

7、处理lambda表达式

  1. 使用lambda表达式的重点是延迟执行

  2. 延迟执行的原因:
    1)在一个单独的线程中运行代码;
    2)多次运行代码;
    3)在算法的适当位置运行代码(例如排序中的比较操作);
    4)发生某种情况时执行代码(例如点击按钮);
    5)只在必要时执行代码;

  3. 常用的函数式接口:
    在这里插入图片描述
    在这里插入图片描述

  4. 注意:如果设计自己的接口,其中只有一个抽象方法,可以用@FunctionalInteface注解标记这个接口,作为函数式接口;

8、再谈Comparator

这节内容看不太懂。。。以后再补;

三、内部类

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

使用内部类的原因主要有两个:
1)内部类可以对同一个包中的其他类隐藏;
2)内部类方法可以访问定义这个类的作用域中的所有数据,包括原本私有的数据;

1、使用内部类访问对象状态

  1. 内部类的对象总有一个隐式引用(除了静态内部类),指向创建它的外部类对象(一般称为outer,注意outer不是Java的关键字,只是用于说明内部类机制);(这个引用在内部类的定义中是不可见的)
  2. 编译器会自动修改所有的内部类构造器,添加一个对应外围类引用的参数(也是不可见的);
  3. 在外围类的方法中,如果构造了内部类的一个对象,编译器就会自动(不可见)将当前外围类的this引用传递给这个构造器;

2、内部类的特殊语法规则

  1. 表达式OuterClass.this表示外围类引用
    例如,可以像下面这样编写TimePrinter内部类的actionPerformed方法:
public void actionPerformed(ActionEvent event){
	...
	if(TimePrinter.this.beep) //这里的TimePrinter.this表示外围类引用
		...
}
  1. 采用以下语法更加明确的编写内部类对象的构造器
    outerObject.new InnerClass(construction parameters)
    (其中outerObject可以替换为任意外围类对象)
//当在外围类内部时
ActionListener listener = this.new TimePrinter();//this是当前外围类对象,一般省略
//当在外围类的作用域之外时
var jabberer = new TalkingClock();//首先构建外围类对象
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();//jabberer是外围类对象
  1. 注意:在外围类的外部,可以这样引用内部类:
    OuterClass.InnerClass
  2. 注意:内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量;(如果这个字段不是一个常量,就可能不唯一)
  3. 注意:内部类不能有static方法

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

通过增加看似精巧有趣、实属没必要的特性,Java是不是也开始走上了让许多语言保守折磨的毁灭性道路?

书中提到不打算就这个问题给出一个完备的答案。

相对于内部类,我认为lambda表达式会更加的简洁,而且用多了也会很熟悉,所以我暂时也跟作者持有相同意见。

  1. 内部类是一个编译器现象,与虚拟机无关。
    编译器将会把内部类转换为常规的类文件,用$(美元符号)分隔外部类名和内部类名,而虚拟机则对此一无所知;
  2. 内部类是如何获得额外的访问权限的?(可以访问外围类的私有数据
    对于TalkingClock类,采用反射机制进行分析,有以下结果:
    可以看到,其中有个静态方法access$0,它将返回作为参数传递的那个对象的beep字段。
    所以当内部类方法调用if(beep),实际上是调用if(TalkingClock.access$0(outer))
Class TalkingClock{
	private int interval;
	private boolean beep;

	public TalkingClock(int,boolean);
	
	static boolean access$0(TalkingClock);//该静态方法名可能稍有不同,也可能是access$000,取决于你的编译器
	public void start();
}

4、局部内部类

按我的理解的话,局部内部类的目的和匿名内部类的原因是类似的,都是因为只利用创建的内部类少次

区别在于
对于匿名内部类(尤其是实现了接口的内部类),它确实是只能创建一次对象;
而对于局部内部类,它是作用域限定于当前块中,在该方法中还是可以创建多个对象的;

局部内部类的例子(将内部类放在某个块中):

public void start(){
	class TimePrinter implements ActionListener{
		public void actionPerformed(ActionEvent event){...}
	}
	
	var listener = new TimePrinter();
	...
}
  1. 声明局部类时不能有访问说明符(即public或private);
  2. 局部类有个很大的优势,即对外部世界(当前块以外的)完全隐藏;

5、由外部方法访问变量

与其它的内部类相比,局部内部类还有一个优点:它们不仅可以访问外围类的数据,还可以访问块(如方法)的局部变量(如方法参数)。

(注意:这些局部变量必须是事实最终变量

例子:

public void start(int interval, boolean beep){
	class TimePrinter implements ActionListener{
		public void actionPerformed(ActionEvent event){
		
			if(beep)	//这里使用的beep是方法的参数变量
				...
		}
	}
	
	var listener = new TimePrinter();
	Timer timer = new Timer(interval,listener);
	timer.start();
}

不过其实这会有一个问题,可能不能访问beep变量的值,让我们先仔细地考察这个控制流程
1)调用start方法
2)调用内部类TimePrinter的构造器,以便初始化对象变量listener;
3)将listener引用传递给Timer构造器,定时器开始计时,start方法推出。此时,start方法的beep参数变量不复存在;
4)1秒之后,actionPerformed方法执行if(beep)…
这里的第4步,可能会出现问题,因为此时的beep参数已经不存在。

为了能够让第四步顺利执行,TimePrinter类在beep参数值消失之前必须将start的beep参数变量复制到类的beep字段

使用反射程序查看TalkingClock$TimePrinter类,会发现结果确实是这样:当创建内部类对象的时候,beep值就会传递给构造器,并存储在val $beep字段中。

class TalkingClock$TimePrinter{
	TalkingClock$TimePrinter(TalkingClock, boolean);
	
	public void actionPerformed(java.awt.event.ActionEvent);
	
	final boolean val$beep;	//为了存储参数变量,在类中创建该字段
	final TalkingClock this$0; //外围类引用
}

6、匿名内部类

  1. 使用匿名内部类的原因:(局部内部类—>匿名内部类)
    假如只想创建实现了某个接口的类的一个对象,可以使用匿名内部类。
    (注意:new的接口或类是已经存在的!而其它的内部类是新建一个类)

例如:

public void start(int interval, boolean beep){
	var listener = new ActionListener() //匿名内部类
	{
		public void actionPerformed(ActionEvent event){
		
			if(beep)
				...
		}
	};
	Timer timer = new Timer(interval,listener);
	timer.start();
}

代码的含义是:创建一个类的对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed在括号{}内定义。

  1. 创建类对象的语法如下:
new SuperType(construction parameters)
{
	inner class methods and data
};

其中,SuperType是类,内部类要拓展这个类。而construciton构造参数将传递给超类构造器,匿名内部类不能有构造器

  1. 创建接口对象的语法如下:
new SuperType()
{
	inner class methods and data
};

其中,SuperType是接口,内部类要实现这个接口。并且,内部类不能有构造参数。

  1. 尽管匿名类不能有构造器,但是可以提供一个对象初始化块;
  2. 双括号初始化”:假设想构造一个只用一次的数组列表,可以使用以下方式(第一个括号代表这是一个匿名内部类,第二个括号代表这是类的初始化块);
new ArrayList<String>(){{add("Hraay"); add("Tony");}}

7、静态内部类

  1. 静态内部类的原因:当内部类不需要有外围类的引用时,可以将内部类声明为static。(注意,只有内部类可以声明为static

  2. 与常规内部类不同,静态内部类可以有静态字段和方法;

  3. 在接口中声明的内部类自动是public和static(我的理解是接口不能创建对象,不会有外围类引用,所以内部类是static的);

  4. 静态内部类的一个例子:当一个方法需要返回两个值的时候,可以创建一个静态内部类(该类有两个字段,并有get方法);

四、服务加载器

有时候会开发一个服务架构的应用。JDK中提供一个加载服务的简单机制,这种机制由Java平台模块系统提供支持,详细内容在本书的卷2第9章。

如何实现服务和加载服务
1)定义一个接口(或者超类),其中包含服务的各个实例应当提供的方法。例如,假设服务要提供加密。

package serviceLoader;

public interface Cipher
{
	byte[] encrypt(byte[] source, byte[] key);
	byte[] decrypt(byte[] source, byte[] key);
	int strength();
}

2)服务提供者可以提供一个或多个实现这个服务的类(实现类可以放在任意的包中,而不一定是服务接口所在的包),例如:

package serviceLoader.imp;

public class CaesarCipher implements Cipher{
	public byte[] encrypt(byte[] source, byte[] key){
		...
	}
	public byte[] decrypt(byte[] source, byte[] key){...
	}
	public int strength(){...}
}

注意:每个实现类必须有一个无参数构造器
3)现在把这些实现类的类名增加到META-INF/services目录下的一个UTF-8编码文本文件中,文件名必须与完全限定类名一致。
我们的例子中,文件META-INF/services/serviceLoader.Cipher必须包含这样一行:
serviceLoader.impl.CaesarCipher
4)完成这个准备工作之后,程序可以如下初始化一个服务加载器(该初始化工作只在程序中完成一次):

public static ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher.class);

5)服务加载器的iterator方法会返回一个迭代器来迭代处理所提供的所有服务实现。或者,使用流查找所要的服务。
6)最后,如果想要得到任何服务实例,只需要调用findFirst:

Optional<Cipher> cipher = cipherLoader.findFirst();

五、代理

这块内容暂时还没看。。待补充。。。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值