读《Java核心技术 卷I》有感之第6章 接口、lambda表达式与内部类

这一章开始就逐渐是Java语言本身的特色内容了,知识点的理解也逐渐开始变困难。换句话说,对于C++的造轮子写法的记忆还是比较顽固的,但是到了Java中发现其设计思路是偏向于提供轮子骨架让你去填充,而不是从头到尾重新造个轮子。

6.1 接口

6.1.1 接口概念

  Java的特色概念降临了,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。换句话说,就是implement实现某接口的类,必须要包含这个接口中所有的方法(实现或默认之后讨论)。其中一个标准接口的定义如下:

public interface Comparable<T>
{
	int compareTo(T other); //T表示泛型,即C++中的typename
}
  • 对于接口而言,其中的所有方法都默认为public类型,所以在声明时不必提供public关键字(这意思应该是可加可不加,但对于强迫症来说似乎还是好点吧)。
  • 接口中不存在实例域,不过在Java SE 8之后已经可以在接口中实现简单方法了,之后也是不允许在接口中实现方法。

  类在实现接口时,基本步骤如下:1)将类声明为实现给定的接口。2)对接口中的所有方法进行定义。具体的实例如下:

public class Employee implements Comparable<Employee>
{
	public int compareTo(Employee other)
	{
		return Double.compare(salary, other.salary);
	}
	...
}
  • 这里需要注意的是在进行接口中的方法实现时,必须添加public声明(因为此时相当于就是一个切实的方法定义,必须为其赋予权限访问标识符)
  • 如果实现接口中的方法时没有标识访问权限,那么编译器会默认这个方法的访问属性即为类中的默认访问属性(类内部可用,同时同包内可见,与protected的访问权限很相似)
  • Java接口的特性体现在:采用实现接口的方式实现接口中的方法(如在Employee类中实现Comparable接口中的compareTo方法),而不是在类中直接定义接口中的方法(如在Employee类中直接定义一个compareTo方法)。原因在于Java是一个强类型语言(即不允许隐式变量类型转换,在调用方法时编译器会直接检查对应类中是否存在该方法),而在使用sort方法对Employee类进行操作时必须保证该类中存在compareTo方法,所以令程序员在编写时都需要通过实现接口的方式来实现compareTo方法(如果让程序员自己定义,万一忘记或写错了compareTo方法则出大问题)

  书中对Comparable在进行子类与父类比较时的情况进行着重讨论,主要体现了这个接口中的方法实现时需要与equals方法相似,保持“反对称”的原则,即x.compareTo(y)与y.compareTo(x)的异常抛出以及效果应该相同。这里书上给出了两点建议:

  • 如果子类之间的比较不一样(比如实例域构成就不同),那就属于不同类对象的非法比较。比较好的解决方案是在每个compareTo方法的开始时执行如下检测(通过反射执行类对象的检测):
if(getClass() != other.getClass()) throw new ClassCastException();
  • 如果一定需要这种对不同比较含义的子类进行比较的方法,那么最好在超类中定义这个compareTo方法,并将其声明为final方法,防止子类重写这个方法搞事。

  总结来说,这一小节主要介绍接口的概念,单从这一小节来说,接口主要用于类进行实现,这些接口中有大量Java本身或者前辈定义好的方法要求被实现。对于实现Java本身的接口而言,是保证其中实现的接口方法能够为系统所用(如sort方法需要使用类中compareTo方法)。对于前辈定义的接口而言,接口就类似于C++中的抽象基类,主要用于定义需要实现的各个虚函数/纯虚函数的名字以及相关的返回值。

6.1.2 接口的特性

  根据前文可以了解到,接口并非一种类,而是一种提供预定义好方法名、参数与返回值的方法的载体,所以不能通过new运算符来实例化一个接口。这里就关系到接口的几个特性:

  • 接口不能被new实例化,但是可以被声明为实现了该接口类对象实例的引用。(通俗点说,接口这个载体不能被new出专属于它的实际内存,但是可以声明为接口类型的指针,指向实现了该接口对象的内存)
  • 接口可以被拓展,即被其他接口继承后增改新的方法于其中。
  • 接口中的所有方法默认为public,接口中的域默认为public static final。
  • 有的接口是作为纯粹提供常量的接口而存在,这与接口设计的初衷(提供预定义名字与返回值的方法)背道而驰,书中建议最好不要使用与制作这种接口。
  • Java类设计重要特性:每个类只能拥有一个超类(extends只能针对一个类),但是可以实现多个接口(implements可以针对多个接口),其中每个接口在书写时通过“,”分割。

6.1.3 接口与抽象类

  前面重点描述了接口的设计初衷,即接口为系统或程序员所提供的预定义了方法名、参数与返回值的方法的载体,在实现时要求程序员implements接口并严格按照其中的方法进行定义,从而确保这些方法确实存在于implements了接口的类中(除了强制限定实现外,接口的方式还易于整体管理,比如所有需要使用sort方法的类都需要implements Comparable后实现其中的compareTo方法)。
  而这一小节的关键是针对“为什么不将接口写成类,然后用继承的方式来实现接口中的方法呢?”这一问题进行阐述。从结果来说,这个问题的答案是因为Java不支持多重继承,即需要继承多个类进行方法重载时显得力不从心。从深层来说,是因为Java在设计之初就放弃了实现多重继承,接口这种方式是在放弃多重继承后,所制造的一种更优于多重继承方式的设计(毕竟C++的多重继承也并非无敌,比如菱形继承问题就很经典)。所以类似C++中需要多重继承时,Java的常规解决方式是继承一个基类进行基本实现,然后通过几个辅助接口进行补充方法的添加。

6.1.4 静态方法

  在Java SE 8 之前,接口之中不允许具体实现所有方法(即保持设计初衷,只负责预定义方法名、参数与返回值),但是有时需要添加接口中的静态方法,所以这些静态方法都是在接口的伴随类之中实现的(比如Path接口的伴随类Paths)。但是在Java SE 8之后,接口之中被允许直接实现静态方法(注意是静态方法,不是所有方法),所以在自身伴随类中的静态方法连着定义和声明也全部在接口类自身中存在。这个改进告诉了两件事情:

  • Java SE 8之后接口的静态方法使用必须通过伴随类,之后在接口中可以直接使用或者仍然可以调用伴随类中的静态方法均可。
  • 建议在自己实现接口时直接在接口方法中进行静态方法定义与声明,没有必要产生冗余的伴随类。

6.1.5 默认方法

  上一小节提到了Java SE 8之后静态方法可以在接口中直接实现,而不需要冗余的伴随类。这一节主要提到的是对于其他常规方法都可以采用在方法声明首位添加“default”修饰符的方式,为该方法添加一个默认实现。比如:

public interface Comparable<T>
{
	default int compareTo(T other) { return 0; }
]

  所以通过这个default方法,Java接口类在SE 8之后的内容又丰富了起来:

  • 通过在接口中的方法声明前显式加上default修饰符后,即可在接口类中对该方法进行默认实现(注意默认方法与静态方法实现的区别,静态方法无需修饰即可在接口中直接定义,默认方法需要显示添加修饰符才可以进行定义)。在其他类implements该接口后如果没有对默认实现了的方法进行重载,那么将会直接使用对应的默认方法,而不是像老版本一样必须强制重载所有的接口中方法。
  • 之前的SE版本中,接口的部分或所有方法都在伴随类中进行默认实现,现在这种伴随类的设计已经逐渐被抛弃,在接口中完成全部默认方式实现与静态方法实现会更好。
  • 默认方法的特色在于实现源代码兼容,即在新版本中对接口进行方法更新(如添加新方法)后,如果不对新加的方法进行默认实现,那么实现了老版本接口的类将会报错(因为没有去实现新方法,而其往往也没必要去实现新方法),这与代码更新的目标相悖。所以将新方法设计为默认方法进行接口更新后,实现老版本接口的类则不会报错。

6.1.6 解决默认方法冲突

  这里说穿了就是超类与接口的方法定义冲突导致的二义性问题的解决方案。

  • 1)超类优先:如果超类方法与接口中默认方法的名字与参数完全一致,则接口中的默认方法将被忽略,(这里注意是默认方法,如果超类中的方法名字、参数和返回值与接口中非默认方法完全相同,就相当于是超类实现了接口中的这个非默认方法,返回值不同可是不构成覆盖的啊注意)
  • 2)接口冲突:如果一个超接口提供了一个默认方法,另一个接口也提供了一个同名而且参数类型完全相同的方法,必须覆盖这个方法来解决冲突(即程序员手动控制到底用哪个默认方法,或者说自己写一个要用的把这两个全覆盖了),具体如下:
public class Student implements Person, Named
{
	public String getName() { return Person.super.getName(); }
}

  对于接口冲突的情况有个很需要注意的点,即并不是两个接口中都实现了默认方法时才会产生二义性,而是一旦有一方实现了默认方法则必然会产生二义性,此时如果必要也需要进行 显示的问题解决。

6.2 接口示例

6.2.1 接口与回调

  回调(callback)是一种常用的程序设计模式,这种模式下,可以指出某个特定事件发生时应该采取的动作。回调的实现方式多种多样,比如Qt中就是通过信号槽的方式实现回调,标准C++中则是传递函数指针的方式提供函数调用,Java则是通过传递实现了接口的对象来实现对象中的函数调用。
  这一节的例子是为Timer计时器设计回调函数,这里写出Timer中对应的构造函数具体实现如下。可以发现其第二个参数是一个ActionListener接口类型的形参,根据前面所说的的接口类型可以引用实现了该接口的类,可见这里的目的就是传入一个实现了ActionListener 接口的对象,Timer将在内部完成一系列操作,从而实现对listener对象中的actionPerformed方法的回调过程。

public Timer(int delay, ActionListener listener)
{
	...
}

  下面则是实现接口ActionListener的类TimerPrinter,这个接口中有且仅有actionPerformed(Action Event)这一个方法,TimerPrinter类的实例将会被传递给Timer。

public class TimerPrinter implements ActionListener
{
	public void actionPerformed(Action Event)
	{
		//Java的类在被执行println时会自动调用toString()函数
		System,out.println("At the tone, the time is " + new Data());
		//响铃
		Toolkit.getDefaultToolkit.beep();
	}
}
		

  最后是将TimerPrinter的实例传递给Timer构造器的过程:

ActionListener listener = new TimePrint();
Timer t = new Timer(10000, listener);
//上面两句等价于
Timer t = new Timer(10000, new TimePrint());

  整个过程很简单,就是当调用某个系统对象(如Timer)时,需要为其定义回调函数,而回调函数的定义则直接通过实现对应的接口来进行定义,最后将实现了对应接口的类的实例对象传递给系统对象,以供其调用其中的回调函数(如Timer调用listener中覆盖的actionPerformed函数)
  (和Qt的Timer写法做个比较,无非是Qt中需要对象类内部自定义用于Timer回调的槽函数,然后用信号槽把Timer信号与回调函数连接起来,而Java中需要对象类实现接口来定义这些回调函数,然后将整个类的对象传递给Timer供给给Timer调用,两者区别在于传递给Timer的东西,一个是函数,一个是整个类,确实是整个类的参数更多更灵活一点)

6.2.2 Comparator接口

  书上这一节的主旨目的是告诉读者两件事:

  • 接口中的方法并不只是作为回调函数或系统默认调用函数来使用,同样也可以单独拉出来做操作。
  • 系统默认调用的函数并不仅仅只会调用传入类中定义的对应方法,也可以通过外部传入的方式,使得系统通过调用外部方法来控制传入类(这里其实和C++的操作很像,控制方式可以外部传入)。

  比如类LenghComparator实现Comparator接口后,可以根据一个引用该类实例的Comparator类型对象,然后通过这个类型对象的内部函数compare进行判断操作,具体如下。不过可笑的是,这里直接使用LenghComparator自己的对象进行函数调用也没啥问题。

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

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

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

  同时也可以将这个类作为参数传递给sort等函数,那么sort函数将会把这个比较类中的compare方法作为比较器,而不是传入类中已经实现的compareTo比较器。比如下面的操作,将会使friends数组通过LenghComparator中的compare方法进行排序,而非String类型自身实现的CompareTo排序方法(String类型是实现了Comparable接口的类)。

String[] friends = {"A", "B", "C"};
Arrays.sort(friends, new LenghComparator());

  从这里可以发现,带有-able后缀的接口应该是用于实现相关系统主要功能的接口,有没有实现往往代表了有没有某功能,比如comparaTo用于排序,clone用于对象克隆。

6.2.3 对象克隆

  本节讨论的主要是Cloneable接口,然后书里这里的描述像极了当时学习C++拷贝构造函数和拷贝赋值运算符时的函数描述。
  书中首先表明了Object类中的clone方法为protected方法,所以使用Object类中的clone方法只能在代码的编写过程中进行使用,但是由于Object方法无法知晓所有子类的数据类型,所有使用该方法进行克隆的过程都属于浅拷贝。而浅拷贝的问题都很明显,基本类型没关系,但是一旦涉及到对象(也就是Java里被封装在内部的指针),就会出现不同类的对象相互引用的问题,这种情况就是使用深拷贝方式的问题。所以类的克隆相关的操作无非是下面循序渐进的三个过程:

  • 1)考虑默认的clone方法是否满足要求,满足则可使用浅拷贝方式,否则考虑下一步;
//浅拷贝类型
class Employee implements Cloneable
{
	public Employee clone() throw CloneNotSupportedException
	{
		return (Employee)super.clone();
	}
	...
}
  • 2)此时只能采用深拷贝方式,考虑是否可以在可变的子对象上调用clone来修补默认的clone方法,是则可以使用深拷贝方式,否则考虑下一步;
//深拷贝类型
class Employee implements Cloneable
{
	public Employee clone() throw CloneNotSupportedException
	{
		Employee cloned = (Employee)super.clone();
		cloned.hireDay = (Date) hireDay.clone();
		return cloned;
	}
	...
}
  • 3)考虑是否不应该使用clone,是则无所谓,否则考虑类相关的整体设计(不实现clone方法是自定义类时的默认选项,因为此时的clone方法在类中都是继承自Object的protected clone方法,在类中表现为private形式,对外而言就是没有实现clone方法)。

  注意一旦确定当前类需要实现克隆操作,无论实现深拷贝还是浅拷贝(因为内部clone方法默认都是private类型的),都需要:

  • 1)实现Cloneable接口;
  • 2)重新定义clone方法,并指定public访问修饰符。

  这里顺便还提到了标记接口与普通接口,其中标记接口不包含任何方法,其唯一作用是允许在类型查询中使用instanceof(为什么不包含方法呢?其实说穿了就是因为Object类中以及包含了对应的方法,所以在标记接口中就没必要写出来了)。而普通接口就是确保一个类实现一个或一组特定的方法,也就是传统的接口定义。
  最后,书中提到了子类的克隆问题:如果类A需要实现clone方法用于其克隆,那么这个clone方法必然被声明为public类型,这会导致继承类A的类B也可以直接调用这个clone方法,但是如果在类B内部自己定义了需要深拷贝的对象,那么此时直接调用类A的clone方法就会出事。这个就是Object类中将自己的clone方法声明为protected方法的原因,但是对于我们自定义的类,在使用clone方法时就需要程序员自己格外注意了。

6.3 lambda表达式

  lambda表达式作为匿名函数的最好体现,在代码简洁度与简化逻辑结构的设计上具有举足轻重的作用,个人认为这个还是很好用的,但是过分在这上面炫技则毫无意义。

6.3.1 为什么引入lambda表达式

  引入lambda表达式的意义就是将一个代码块传递给某个对象。比如前面所提的定时器操作与排序操作,我们在编写时都是通过类实现接口,从而定义之中的actionPerformed方法或者compareTo方法,然后将类对象实例传递给Timer或者sort之中。但是对于Timer与sort本身而言,它们真正需要的其实只有actionPerformed方法或者compareTo方法,传递进来的类对象的其他东西往往都不太会使用。lambda表达式的引用就是为了实现传递这些代码段,而非整个对象。
  看到这里不禁有感而发,Java的设计者在最开始的设计之初一直秉持着Java是面向对象的设计语言思路,对于所有的东西都要求有对象,所以在接口操作时也是实现接口然后传递对象。这与C++中的情况并不完全一致,因为C++中一直都可以通过函数指针的方式将自己的函数直接传递出去使用,即并不是一直都需要对象。所以C++中的lambda表达式往往是作为匿名函数的情况存在,而Java中的lambda表达式反而是将传递方法与匿名函数糅合在了一起,形成了一种折中的,不破坏Java设计者“不将类中函数单独传递出去”初衷的策略。
  只能说,每种语言的策略方式有好有坏,并不能因为一些原因就疯狂贬低某一门原因,毕竟语言一直都只是所谓的工具,每种语言都有自己的强项弱项。对于一个码农来说,想要提升自己,拘泥于现状,拘泥于一门语言是行不通,要走出去,看到别的语言的设计思路与精华,才能体会编程的美妙之处。

6.3.2 lambda表达式的语法

  lambda表达式的标准语法:(参数)-> 表达式,然后有些特殊操作:

(String first, String second)->
{
	return first.length() - second.length();
}
  • 如果lambda表达式没有参数,仍然需要提供空括号;
()->{System.out.println("test"):
  • 如果能够推导出lambda表达式的参数类型,则可以省略类型描述;
Comparator<String> comp = (first, second)->first.length() - second.length();
  • 如果方法只有一个参数,而且这个参数的类型可以推导得出,可以省略参数小括号。
ActionListener listener = event->System.out.println("The time is" + new Date());
  • 通常可以省略lambda表达式的返回类型,因为lambda表达式的返回类型往往会由上下文推导得出。
Comparator<String> comp = (String first, String second)->first.length() - second.length();

  我个人感觉在刚开始写这些东西的时候,还是尽量能写的都写为好,不然省略了看的头皮发麻。

6.3.3 函数式接口

  对于只有一个抽象方法的接口,需要这种接口的对象时,可以提供一个lambda表达式,这种接口可以被称为函数式接口(其实lambda表示式就是去实现接口中的那个抽象方法,可以理解为lambda表达式的这块代码段被放置到了抽象方法之中,而抽象方法的形参会传递给lambda表达式的形参)。

Timer t = new Timer(1000, event -> {System.out.println("the times is" + new Date());
  • 这里特别注意是只有一个抽象方法的接口而不是只有一个方法的接口,因为有些接口会复写Object类中的toString或clone这样的基础功能方法,导致一个接口中并不是所有的方法都是抽象的。
  • 这里仍要注意只有函数式接口可以接受lambda表达式,别的接口都不可以。

6.3.4 方法引用

  方法引用的目的是直接使用现成的方法来完成需要传递个某个代码的某个动作,换句话说就是lambda表达式足够简单时可以使用方法引用的方式来替换掉,具体如下:

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

  这么写的含义其实就是把接口中需要实现的那个抽象方法的形参传递给了方法引用的方法,而如果方法引用的方法有多个重载,此时编译器会根据上下文找出最匹配的那个方法。

6.3.5 构造器引用

  构造器引用说穿了也是方法引用的一种,只不过引用构造器时是使用new表示所有的构造器,如果有多个构造器编译器也会根据上下文选用最匹配的那个方法。

Stream<Person> stream = names.stream().map(Person::new);

6.3.6 变量作用域

  本节表示的意思就是lambda表达式在编写时可以访问外围方法或类中的变量,其实现的机理在于lambda表达式会捕获其需要使用的变量,并将其存储等待使用(这里被称为闭包特性)。不过对于捕获的值有几个很关键的限制:

  • 捕获值只能是值本身不会改变的变量(如在lambda表达式内部修改捕获值,在多线程时会出问题);
  • 捕获值如果在外部改变也是不合法的,在多线程时也会出现问题;
  • lambda表达式捕获的变量必须是最终变量,也就是说这个变量在初始化后就不会被赋予新值。
  • lambda表达式的体与嵌套块具有相同的作用域,所以其内部发生命名冲突的问题也是存在的,同时其this的作用对象也就是嵌套其本身的那个对象,与常规写法没有区别。

6.3.7 处理lambda表达式

  本节主要描述了对于lambda表达式存在的意义,即延时执行策略。但是我个人认为延时执行其实算是附加意义,lambda表达式最直观的意义还是匿名函数,也就是代码简洁。
  这里简单说明一下延迟执行概念的意义:lambda表达式的作用是将这块代码段进行传递,也就是说将表达式中的代码赋予到某个函数式接口之中,而这个函数式接口怎么调用、何时调用以及调用次数都是可以之后再进行规定的。网上有一个经典的日志记录问题就是讲的lambda表达式的延迟执行意义,有兴趣可以搜搜看。
  之后本节中还列举出了两个表的标准常用函数式接口,这种标准化接口的主旨意义是给予我们足够的模板,保证在自己编写代码时需要使用函数式接口时,尽量使用系统写的,而不是自己定义(可能觉得自己定义的会不太好?)。其中常用函数式接口适合多功能性,而基本类型的函数式接口适主要作用在于减少基本类型的自动装箱,提升性能。

6.3.8 再谈Comparator

  本节的描述更偏向于应用性,也就是说Comparator并非只能通过复写compare函数来实现比较,该接口内部系统已经预定义了大量静态方法来进行辅助比较:

  • 比如comparing方法和thenComparing方法,可以将传入方法的实参类型进行类型提取,从而通过该类型进行比较;
  • 比如nullsFirst与nullsLast适配器用于处理comparing等方法提取类型为空的情况,其中需要为适配器的形参提供相关类型的比较器,常规的有naturalOrder和reverseOrder等实例方法。

6.4 内部类

  内部类(inner class)是定义在一个类中的类,其存在的原因有三:

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据;
  • 内部类可以对同一个包中的其他类隐藏起来(这个个人认为比较关键);
  • 当想定义一个回调函数同时不想大量编码时,使用内部类比较便捷。

  内部类的特点在于其对象具有隐式引用,也就是说起能够使用实例化该内部对象的外围类对象。不过static内部类没有这种附加指针,所以与C++的嵌套类就很相近了。

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

  本节的要点是体现内部类能够直接访问外围类的所有域和方法,包括私有的,所以这里为了便于理解也写一个简单的外部类和内部类来表示一下:

public TalkingClock
{
	private int interval;
	private boolean beep;

	public TalkingClock(int interval, boolean beep)
	{
		this.interval = interval;
		this.beep = beep;
	}

	public void start()
	{
		ActionListener listener = new TimePrinter();
		Timer t = new Timer(interval, listener);
		t.start();
	}

	public class TimerPrinter implements ActionListener
	{
		public void actionPerformed(ActionEvent event)
		{
			System.out.println("the time is " + new Date());
			if(beep)
				Toolkit.getDefaultToolkit().beep();
			}
		}
	}
}

6.4.2 内部类的特殊语法规则

  通过这节可以发现内部类的语法规则确实还是有点复杂:

  • 表达式 outerClass.this 表示外围类的引用(如TalkingClock.this.beep);
  • 表达式 outerObject.new InnerClass(construction parameters) 表示当前外部对象outerObject的内部类InnerClass的构造器(如this.new TimerPrinter());
  • 如果内部类为public形式,那么表达式 OuterClass.InnerClass 用于在外部类的作用域之外,引用外部类内部的内部类(如TalkingClock.TimePrinter);
  • 内部类中声明的所有静态域都必须是final,这是为了保证不同的外部对象在使用内部类的静态域时都保证是同一个值;
  • 内部类不能有static方法,这个没有解释原因,用书上的话就是设计者觉得这样会使内部类太复杂。

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

  内部类是一种编译器现象,与虚拟机无关。编译器会将内部类翻译成用$(美元符号)分隔外部类名与内部类名的常规类文件(如A的内部类B,会被翻译为A $ B.class),虚拟机其实对内部类一无所知,对其而言就是两个类罢了。
  内部类实现外部类调用的根本方式是在其内部生成一个附加的实例域this$0(注意是内部类访问了外部类的私有域后才会生成),这所导致的结果是使得外部类所在包中的其他类可以通过某种方式,利用内部类访问外部类的私有数据域。

6.4.4 局部内部类

  局部内部类即指在外部类的方法中所创建的类,而非在外部类的类作用域中所创建的。局部内部类的特点在于:

  1. 不能用public和private访问说明符进行声明,其作用域被限定于声明这个局部类的方法块中。
  2. 局部内部类完全地被隐藏,只有声明它的方法中知道,外部类自身也不知道该类的存在,但是这个局部内部类可以访问外部类(因为他的作用域就是依赖于声明方法)。
public void start(int interval, boolean beep)
{
	class TimerPrinter implements ActionListener
	{
		public void actionPerformed(ActionEvent event)
		{
			System.out.println("the time is " + new Date());
			if(beep)
				Toolkit.getDefaultToolkit().beep();
		}
	}

	ActionListener listener = new TimePrinter();
	Timer t = new Timer(interval, listener);
	t.start();
}

6.4.5 由外部方法访问变量

  局部内部类还有一个特点是能够访问局部变量,典型的就是声明该局部类的方法的输入参数。其实现方式是在局部内部类的内部建立一个final类型的变量,命名类似于"val $ 局部变量名"(如final boolean val$beep),每当方法中输入局部变量时,都会刷新局部内部类的那个final内部域,即通过存储的方式实现对局部变量的调用工作。、

6.4.6 匿名内部类

  匿名内部类属于局部内部类的再深一层,即当只需要创建这个局部内部类的一个对象时,不需要对这种局部内部类进行命名,而是直接在定义接口时,将实现该接口内部方法的代码(这种往往对应函数式接口,即只有一个需要定义的方法)以大括号的方式在new 接口名()后面放置,这种写法其实就是将构造器参数(大括号里的代码)传递给了超类构造器(也就是接口的构造器)
  这种new 类名A(){}的方式属于Java的特色操作,作用就是建立类A的一个匿名对象,其中在大括号{}中可以提供对A的这个匿名对象的各种操作,比如构造器操作、方法调用抑或是复写方法等等,大括号里的东西都可以理解为使用这个匿名对象在做某某操作。

public void start(int interval, boolean beep)
{
	ActionListener listener = new ActionListener()
	{
		System.out.println("the time is " + new Date());
		if(beep)
			Toolkit.getDefaultToolkit().beep();
	}
	Timer t = new Timer(interval, listener);
	t.start();
}

  当然这种写法已经被lambda表达式完爆,在函数式接口领域里,确实还是lambda表达式吊打这种传统的匿名内部类(匿名内部类还是传统的传递对象,这也就是为什么要final内部域来存储局部变量的原因,而lambda表达式只需要传递方法)。

public void start(int interval, boolean beep)
{
	Timer t = new Timer(interval, event->
	{
		System.out.println("the time is " + new Date());
		if(beep)
			Toolkit.getDefaultToolkit().beep();
	});
	t.start();
}

6.4.7 静态内部类

  静态内部类,通俗点来说很接近于C++中的嵌套类,其无法访问外部类的对象(换句话说就是内部没有外部类的this引用),不过在通过static关键字进行定义时,主要是表示这个类是静态类,还是需要通过public和private来限定类的作用域。这种内部类的特定有:

  • 静态内部类可以有自己的静态域和方法,而其他常规内部类都不能用;
  • 声明在接口中的内部类自动成为public static类;
  • 静态内部类只能在静态方法中构造,换句话说,静态方法只能使用类中的静态类型,包括静态域、静态方法和静态内部类
class ArrayAlg
{
	public static class Pair
	{
		private double first;
		private double second;
	
		public Pair(double f, double s)
		{
			first = f;
			second = s;
		}
		
		public getFirst()
		{
			return first;
		}

		public getSecond()
		{
			return second;
		}
	}
	
	public static Pair minmax(double[] values)  //注意静态内部类的返回也是static class
	{
		double min = Double.POSITIVE_INFINITY;
		double max = Double.NEGATIVE_INFINITY;
		for(double v : values)
		{
			if(min > v) min = v;
			if(max < v) max = v;
		}
		return new Pair(min,max);
	}	
}

6.5 代理

  利用代理可以在运行时创建一个实现了一组给定接口的新类,这种功能只有在编译时无法确定需要所实现的接口时才有必要使用。

6.5.1 何时使用代理

  有一个实现了多个接口的类(当然可能也就实现了一个),其将要被用到多个地方进行使用所以实现了多个接口,但是问题在于这样就无法在编译时搞清楚这个类到底应该是哪个接口的实现(也就是所谓的确切类型),而代理类就是用于在运行时创建全新的类,这样的代理类能够实现指定的接口,其(特别的)具有下列方法:

  • 指定接口所需要的全部方法;
  • Object类中的全部方法,例如toString、equals等;

  但是最大的特点在于:不能在运行时定义这些方法的新代码,而是要提供一个调用处理器(invocation handler),这个所谓的调用处理器是程序员需要去完成的类对象,其必须实现InvocationHandler接口,而这个接口中只有一个方法:

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

  换句话说,代理类中方法的真正实现是通过使用调用处理器中的invoke方法,该方法会根据传入的method、args信息进行调用,所以调用处理器中的invoke方法必须给出处理调用的方式,最常见的就是return m.invoke(target, args)。

6.5.2 创建代理对象

  创建一个代理对象,需要使用Proxy类的newProxyInstance方法,调用该方法时需要提供三个参数:

  • 一个类加载器(class loader):可以用下载的,但是默认用null表示使用默认的类加载器;
  • 一个Class对象数组:每个元素都是需要实现的接口;
  • 一个调用处理器。

  这里举个典型的例子,在使用proxy类进行方法调用时,会调用对象handler(也就是类TraceHandler)中的invoke方法,从而实现代理功能:

Object value = ...;
InvocationHandler handler = new TraceHandler(value);  //TraceHandler人为实现接口定义
Class[] interfaces = new Class[] { Comparable.class };
Object proxy = Proxy.newProxyInstance(null, interfaces, handler);
  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

方寸间沧海桑田

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值