《Java核心技术 卷Ⅰ》 第6章 接口、lambda表达式与内部类前端
接口技术,这种技术主要用来描述类具备什么功能,而并不给出每一个功能的具体实现。一个类能够实现(implement)一个或多个接口,并在须要接口的地方,随时使用实现了相应接口的对象。java
在Java程序设计语言中,接口不是类,是对类的一组需求的描述,这些类要听从接口描述的统一格式进行定义。
Arrays
类中的sort
方法承诺能够对对象数组进行排序,但要求知足下列条件,对象所属的类**必须实现了Comparable
接口。git
public interface Comparable { int compareTo(Object other) }
这就是说,任何实现Comparable
接口的类都须要包含compareTo
方法,而且这个方法的参数必须是一个Object
对象,返回一个整型数值;好比调用x.compareTo(y)
时,当x
小于y
时,返回一个负数;当x
等于y
时,返回0;不然返回一个正数。程序员
在 Java SE 5中,Comparable
接口改进为泛型类型。github
public interface Comparable<T> { int compareTo(T other); // 参数拥有类型T }
例如在实现Comparable<Employee>
接口类中,必须提供int compareTo(Employee other)
方法。数组
接口中的全部方法自动地属于public
,所以,在接口中声明方法时,没必要提供关键字public
。安全
提供实例域和方法实现的任务应该由实现接口的那个类来完成。数据结构
在这里能够将接口当作是没有实例域的抽象类。闭包
如今但愿用Arrays
类的sort
方法对Employee
对象数组进行排序,Employee
类必须实现Comparable
接口。并发
为了让类实现一个接口,一般须要下面两个步骤:
将类声明为实现某个接口,使用关键字implements
:
class Employee implements Comparable { ... public int compareTo(Object otherObject) { Employee other = (Employee) otherObject; return Double.compare(salary, other.salary); } ... }
这里使用了静态Double.compare
方法,若是第一个参数小于第二个参数,它会返回一个负值,相等返回0,不然返回一个正值。
虽然在接口声明中,没有将compareTo
方法声明为publuc
,这是由于接口中全部方法都自动地是public
,可是,在实现接口时,必须把方法声明为public
,不然编译器将认为这个方法的访问属性是包可见性,即类的默认访问。
能够为泛型Comparable
接口提供一个类型参数。
class Employee implements Comparable<Employee> { ... public int compareTo(Employee other) { return Double.compare(salary, other.salary); } ... }
为何不能再Employee
类直接提供一个compareTo
方法,而必须实现Comparable
接口呢?
主要缘由是Java是一种强类型(strongly type)语言,在调用方法时,编译器将会检查这个方法是否存在。
在sort方法通常会用到compareTo
方法,因此编译器必须确认必定有compareTo
方法,若是数组元素类实现了Comparable
接口,就能够确保拥有compareTo
方法。
接口不是类,尤为不能用new
实例化接口:
x = new Comparable(...); // Error
尽管不能构造接口的对象,却能声明接口的变量:
Comparable x; // OK
接口变量必须引用实现了接口的类对象:
x = new Employee(...); // OK
也可使用instanceof
检查一个对象是否实现了某个特定的接口:
if(x instanceof Comparable) { ... }
与类的继承关系同样,接口也能够被扩展。
这里容许存在多台从具备较高通用性的接口到较高专用性的接口的链。
假设有一个称为Moveable
的接口:
public interface Moveable { void move(double x, double y); }
而后,能够以它为基础扩展一个叫作Powered
的接口:
public interface Powered extends Moveable { double milesPerGallon(); }
虽然接口中不能包含实例域或者静态域,可是能够定义常量:
public interface Powered extends Moveable { double milesPerGallon(); double SPEED_LIMIT = 95; // a public static final constant }
与接口中的方法自动设置为public
同样,接口中的域被自动设为public static final
。
尽管每一个类只能拥有一个超类,但却实现多个接口。
class Employee implements Coneable, Comparable { ... }
你可能会问:为何这些功能不能由一个抽象类实现呢?
由于使用抽象类表示通用属性存在这样的问题:每一个类只能扩展于一个类,没法实现一个类实现多个接口的需求。
class Employee extends Person implements Comparable { ... }
在 Java SE 8 中,容许在接口中增长静态方法。
虽说这没有什么不合法的,只是这有违接口做为抽象规范的初衷。
一般的作法是将静态方法放在伴随类中。在标准库中,有成对出现的接口和实用工具类,如Collection
/Collections
或Path
/Paths
。
虽然Java库都把静态方法放到接口中也是不太可能,可是实现本身接口时,不须要为实用工具方法另外提供一个伴随类。
能够为接口方法提供一个默认实现,必须用default
修饰符标记方法。
public interface Comparable<T> { default int compareTo(T other) { return 0; } }
默认方法的一个重要用法是接口演化(interface evolution)。
以Collection
接口为例,这个接口做为Java
的一部分已经好久了,假如好久之前提供了一个类:
public class Bag implements Collection { ... }
后来,在Java SE 8中,为这个接口增长了一个stream
方法。若是stream
方法不是默认方法,那么Bag
类将不能编译——由于它没有实现这个新方法。
为接口增长一个非默认方法不能保证“源代码兼容”(source compatible)。
若是一个接口中把方法定义为默认方法,而后又在超类或另外一个接口中定义了一样的方法,会发生什么状况?
解决这种二义性,Java的规则是:
着重看一下第二个规则,考虑另外一个包含getName
方法的接口:
interface Named { default String getName() { return getClass().getName() + "_" + hashCode(); } }
如今有一个类同时实现了这两个接口,这个时候须要程序员来解决这个二义性,在这个实现的方法中提供一个接口的默认getName
方法。
class Student implements Person, Named { public String getName() { return Person.super.getName(); } }
就算Named
接口并无getName
的默认方法,一样须要程序员去解决这个二义性问题。
上面的是两个接口的命名冲突。
如今考虑另外一种状况:一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。
class Student extends Person implements Named { ... }
这种状况下只会考虑超类的方法,接口全部默认方法会被忽略。
回调(callback),能够指出某个特定事件时应该采起的动做。
java.swing
包中有一个Timer
类,可使用它在到达给定的时间间隔发送通告。
在构造定时器时,须要设置一个时间间隔,并告知定时器,达到时间间隔时须要作什么。
其中一个问题就是如何告知定时器作什么?在不少语言中,是提供一个函数名,可是,在Java标准类库中的类采用的是面向对象方法,它将某个类的对象传递给定时器,而后定时器调用这个对象的方法。
定时器须要知道调用哪个方法,并要求传递的对象所属的类实现了java.awt.event
包的ActionListener
接口:
public interface ActionListener { void actionPerformed(ActionEvent event); }
当到达指定时间间隔,定时器就调用actionPerformed
方法。
使用这个接口的方法:
class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println(...); ... } }
其中接口方法的ActionEvent
参数提供了事件的相关信息。
接下来构造类的一个对象,并传递给Timer
构造器。
ActionListener listener = new TimePrinter() Timer t = new Timer(10000, listener); t.start(); // 启动定时器
能够对一个字符串数组排序,是由于String
类实现了Comparable<String>
,并且String.compareTo
方法能够按字典顺序比较字符串。
如今须要按长度递增的顺序对字符串进行排序,咱们确定不能对String
进行修改,就算能够修改咱们也不能让它用两种不一样的方式实现compareTo
方法。
要处理这种状况,Arrays.sort
方法还有第二个版本,一个数组和一个比较器(comparator)做为参数,比较器实现了Comparator
接口的类的实例。
public interface Comparator<T> { int compare(T first, t second); }
按字符串长度比较,能够定义一个实现Comparator<String>
的类:
class LengthComparator implements Comparator<String> { public int compare(String first, String second) { return first.length() - second.length(); } }
具体比较时,创建一个实例:
Comparator<String> comp = new LengthComparator(); // comp.compare(words[i], words[j]) Arrays.sort(friends, comp);
Cloneable
接口,指示一个类提供了一个安全的clone
方法。
Employee original = new Employee(...); Employee copy = original.clone(); copy.raiseSalary(10); // no changes happen to original
clone
方法是Object
的一个protected
方法,代码不能直接调用这个方法(指的是Object
的这个方法)。
固然,只有Employee
类能够克隆Employee
对象,可是默认的克隆操做是浅拷贝,即并无克隆对象中引用的其余对象。
浅拷贝可能会产生问题么?这取决于具体状况:
通常来讲子对象都是可变的,因此须要定义clone
方法来创建一个深拷贝,同时克隆全部子对象。
对于每个类,须要肯定:
clone
是否知足要求clone
来修补默认clone
clone
实际上第3个选项是默认选项(这句话没有太读懂)。
若是选第1个或者第2个,类必须:
Cloneable
接口clone
方法,并指定public
访问修饰符子类虽然能够访问Object
受保护的clone
方法,可是子类只能调用受保护的clone
方法来克隆它本身的对象。
必须从新定义clone
为public
,才能容许全部方法克隆对象。
Cloneable
接口是一组标记接口,其余接口通常确保一个类实现一个或一组特定的方法,标记接口不包含任何方法,它的惟一做用就是容许在类型查询中使用instanceof
。
即时clone
的默认(浅拷贝)实现可以知足要求,仍是须要实现Cloneable
接口,将clone
从新定义为public
,再调用super.clone()
。
class Employee implements Cloneable { // raise visibility level to public, change return type public Employee clone() throws CloneNotSupportedExcption { return (Employee) super.clone(); } }
与浅拷贝相比,这个clone
并无增长任何功能,只是让方法变为公有,要创建深拷贝。
class Employee implements Cloneable { ... public Employee clone() throws CloneNotSupportedExcption { // Obejct.clone() Employee cloned = (Employee) super.clone(); //clone mutable fields cloned.hireDay = (Date) hireDay.clone(); return cloned; } }
若是一个对象调用clone
,但这个对象类没有实现Cloneable
接口,Object
的clone
方法就会抛出一个CloneNotSupportedExcption
,Employee
和Date
类实现了Cloneable
接口,因此不会抛出异常,可是编译器并不知道这点,因此声明异常最好还要加上捕获异常。
class Employee implements Cloneable { // raise visibility level to public, change return type public Employee clone() throws CloneNotSupportedExcption { try { Employee cloned = (Employee) super.clone(); ... } catch(CloneNotSupportedExcption e) { return null; } // 由于实现了Cloneable,因此这并不会发生 } }
必须小心子类的克隆。
例如,一旦Employee
类定义了clone
,那么就能够用它来克隆Manager
对象(由于在Employee
类中的clone
已是public
了,能够直接使用Manager.clone()
)。
但Employee
的clone
必定能完成克隆Manager
对象的工做么?
这取决于Manager
类的域:
clone
方法让它正常工做出于后者的缘由,在Object
类中的clone
方法声明protected
。
一种表示在未来某个时间点执行的代码块的简洁方法。
使用lambda表达式,能够用一种精巧而简洁的方式表示使用回调或变量行为的代码。
lambda表达式是一个可传递的代码块,能够在之后执行一次或屡次。
以前的监听器和后面的排序比较例子的共同点是:都是把一个代码块传递到某个对象(定时器或者是sort
方法),而且这个代码块会在未来某个时间调用。
考虑以前的按字符串长度排序例子:
first.length() - second.length()
Java是一种强类型语言,因此还要指定他们的类型:
(String first, String second) -> first.length() - second.length() // 隐式return 默认返回这个表达式的结果
这就是一个lambda表达式,一个代码块以及变量规范。
若是代码要完成的计算不止一条语句,能够像写方法同样,把代码放在{}
中,并包含显式的return
语句。
(String first, String second) -> { if(first.length() < second.length()) return -1; else if(first.length() > second.length()) return 1; else return 0; }
一些省略形式的表达:
须要注意的地方:
return
,那就要确保每一个分支都有一个return
,不然是不合法的Java中已经有不少封装代码块的接口,好比ActionListener
或Comparator
,lambda表达式与这些接口兼容。
对于只有一个抽象方法的接口,须要这种接口的对象时,就能够提供一个lambda表达式,这种接口称为函数式接口(functional interface)。
考虑以前的Arrays.sort
方法,其中第二个参数须要一个Comparator
实例,函数式接口使用:
Arrays.sort(words, (first, second) -> first.length() - second.length());
在底层,Arrays.sort
方法会接收实现了Comparator<Strng>
的某个类的对象,在这个对象上调用compare
方法会执行这个lambda表达式的体。
最好把lambda表达式看做一个函数,而不是一个对象,并且要接收lambda表达式能够传递到函数式接口。
lambda表达式能够转换为接口,这让lambda表达式颇有吸引力,具体的语法很简单:
Timer t = new Timer(10000, event -> { System.out.println(...); ... });
与使用实现了ActionListener
接口的类相比,这个代码可读性好不少。
实际上,在Java中,对lambda表达式所能作的也只是能转换为函数式接口,甚至不能把lambda表达式赋给类型为Object
的变量,Object
不是一个函数式接口。
有时,可能已经有现成的方法能够完成你想要传递到其余代码的某个动做。
好比只要出现一个定时器事件就打印这个事件对象:
Timer t = new Timer(10000, event -> System.out.println(event));
可是若是直接把println
方法传递给Timer
构造器就更好了:
Timer t = new Timer(10000, System.out::println);
表达式System.out::println
就是一个方法引用(method reference),它等价于lambda表达式x - > System.out.println(x)
。
考虑一个排序例子:
Arrays.sort(words, String::compareToIgnoreCase);
主要有3种状况:
object::instanceMethod
Class::staticMethod
Class::instanceMethod
前面两种等价于提供方法参数的lambda表达式,好比System.out::println
等价于x -> System.out.println(x)
,以及Math::power
等价于(x, y) -> Math.power(x, y)
。
对于第3种,第1个参数会成为方法的目标,例如String::compareToIgnoreCase
等价于(x, y) -> x.compareToIgnoreCase(y)
。
能够在方法引种中使用this
,super
也是合法的,好比super::instanceMethod
,使用this
做为目标,会调用给定方法的超类版本。
构造器引用与方法引用相似,只不过方法名为new
,例如Person::new
是Person
构造器的一个引用,具体选择Person
多个构造器中的哪个,这个取决于上下文。
如今有一个字符串列表,你能够把它转换为一个Person
对象数组,为此要在各个字符串上调用构造器。
ArrayList<String> names = ...; Stream<Person> stream = names.stream().map(Person::new); List<Person> people = stream.collect(Collectors.toList());
stream
、map
和collect
方法会在卷Ⅱ的第1章讨论。
如今的重点是map
方法会为各个列表元素调用Person(String)
构造器,这里编译器从上下文推导出这是在对一个字符串调用构造器。
能够用数组类型创建构造器引用,int[]::new
是一个构造器引用,有一个参数,就是数组的长度,这等价于x -> new int[x]
。
Java有一个限制:没法构造泛型类型T的数组。
数组构造器引用对于克服这个限制颇有用。
假设须要一个Person
对象数组,Stream
接口有一个toArray
方法能够返回Object
数组:
Object[] people = stream.toArray();
可是用户想要一个Person
引用数组,流库利用构造器引用解决了这个问题:
Person[] people = stream.toArray(Person[]::new);
toArray
方法调用构造器得到一个正确类型的数组,而后填充这个数组并返回。
一般可能想在lambda表达式中访问外围方法或类中的变量。
public static void repeatMessage(String text, int delay) { ActionListener listener = event -> { System.out.println(text); ... }; new Timer(delay, listener).start(); }
具体调用:
repeatMessage("Hello", 1000);
lambda表达式中的变量text
,并非在这个lambda表达式中定义的,可是这其实有问题,由于代码可能会调用返回好久之后才运行,而那时这个参数变量已经不存在了,该如何保留这个变量?
重温一下lambda表达式的3个部分:
上面的例子中有1个自由变量text
。
表示lambda表达式的数据结构必须存储自由变量的值,也被叫作自由变量被lambda表达式捕获(captured)。
能够把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。
关于代码块以及自由变量有一个术语:闭包(closure),Java中lambda表达式就是闭包。
在lambda表达式中,只能引用值不会改变的变量,好比下面这种就是不合法的:
public static void countDown(int start, int delat) { ActionListener listener = event -> { start--; // Error: Can't mutate captured variable System.out.println(text); ... }; new Timer(delay, listener).start(); }
若是在lambda表达式中改变变量,并发执行多个操做时就会不安全(具体要见第14章并发)。
另外若是在lambda表达式中引用变量,而且这个变量在外部改变,这也是不合法的:
public static void repeat(String text, int count) { for(int i = 1; i <= count; i++) { ActionListener listener = event -> { System.out.println(i + ":" + text); // Error: Can't refer to changing i ... }; new Timer(1000, listener).start(); } }
因此简单来讲规则就是:lambda表达式中捕获的变量必须其实是最终变量(effectively final),即这个变量初始化以后就再也不赋新值。
lambda表达式的体与嵌套块有相同的做用域,因此在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
Path first = Path.get("/usr/bin"); Comparator<String> comp = (first, second) -> fisrt.length() - second.length(); // Error: Variable first already defined
固然在lambda表达式中也不能有同名的局部变量。
在lambda表达式中使用this关键字时,是指建立这个lambda表达式的方法的this参数。
public class Application() { public void init() { ActionListener listener = event -> { System.out.println(this.toString()); ... } } }
this.toString()
会调用Application
对象的toString
方法,而不是ActionListener
实例的方法,因此在lambda表达式中this
的使用并无什么特殊之处。
内部类(inner class)定义在另外一个类的内部,其中的方法能够访问包含它们的外部类的域。
内部类主要用于设计具备相互协做关系的类集合。
使用内部类的主要缘由:
将从如下几部分介绍内部类:
内部类的语法比较复杂。
选择一个简单的例子:
public class TalkingClock { private int interval; private boolean beep; public TalkingClock(int interval, boolean beep) { ... } public void start() { ... } // an inner class public class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { System.out.println(...); if(beep) Toolkit.getDefaultToolkit().beep(); } } }
TimePrinter
类位于TalkingClock
类内部,并不意味着每一个TalkingClock
对象都有一个TimePrinter
实例域。
TimePrinter
类没有实例域或者beep
变量,而是引用了外部类的域里的beep
。
其实内部类的对象总有一个隐式引用,它指向了建立它的外部类对象,这个引用在内部类的定义中不可见。
这个引用是在对象建立内部类对象的时候传入的this
,编译器经过内部类的构造器传入到内部类对象的域中。
// 由编译器插入的语句 ActionListener listener = new TimePrinter(this);
TimePrinter
类能够声明为私有的,这样只有TalkingClock
方法才能构造TimePrinter
对象。只有内部类能够是私有的,常规类只能够是包可见和公有可见。
使用外围类引用的语法为OuterClass.this
。
例如以前的actionPerformed
方法:
public void actionPerformed(ActionEvent event) { ... if(TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep(); }
反过来,能够用`outerObject.new InnerClass(construction parameters)更加明确地编写内部类对象的构造器:
// ActionListener listener = new TimePrinter(this); ActionListener listener = this.new TimePrinter();
一般来讲this
限定词是多余的,可是能够经过显式命名将外围类引用设置为其余对象,好比当TimePrinter
是一个公有内部类时,对于任意的语音时钟均可以构造一个TimePrinter
:
TalkingClock jabberer = new TalkingClock(1000, true); TalkingClock.ActionListener listener = jabberer.new TimePrinter();
上面的状况是在外围类的做用域以外,因此引用的方法是OuterClass.InnerClass
。
注意:内部类中声明的全部静态域都必须是final
,由于咱们但愿一个静态域只有一个实例。不过对于每一个外部对象,会分别有一个单独的内部类实例,若是这个域不是final
,它可能就不是惟一的。
若是一个类只在一个方法中建立了对象,能够这个方法中定义局部类。
public void start() { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { ... } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start(); }
局部类不能用public
或private
,它的做用域被限定在生命这个局部类的块中。
可是有很是好的隐蔽性,除了start
方法,没有任何方法知道TimePrinter
类的存在。
局部类还有一个优势:他们还能访问局部变量,可是这些局部变量必须是final
,即一旦赋值就不会改变。
下面的例子相比以前进行了一些修改,beep
再也不是外部类的一个实例域,而是方法传入的参数变量:
public void start(int interval, final boolean beep) { class TimePrinter implements ActionListener { public void actionPerformed(ActionEvent event) { ... if(beep) ...; ... } } ActionListener listener = new TimePrinter(); Timer t = new Timer(interval, listener); t.start(); }
先说明一下这里的控制流程:
start(int, boolean)
TimePrinter
的构造器,初始化listener
listener
引用传递给Timer
构造器t
开始计时start(int, boolean)
方法结束,此时beep
参数变量不复存在actionPerformed
方法执行if(beep) ...
为了让actionPerformed
正常运行,TimePrinter
类在beep
域释放以前将内部类中要用到的beep
域用start
方法的局部变量beep
进行备份(具体实现方式是编译器给内部类添加了一个final
域用来保存beep
)。
编译器检测对局部变量的访问,为每个量创建相应的数据域,并将局部变量拷贝到构造器中,以便将这些数据域初始化为局部变量的副本。
至于beep
参数前的final
,是由于局部类的方法只能引用定义为final
的局部变量,从而使得局部变量与局部类中创建的拷贝保持一致。
假设只建立这个局部类的一个对象,就没必要命名了,这种类称为匿名内部类(anonymous inner class)。
public void start(int interval, boolean beep) { ActionListener listener = new ActionListener() { public void actionPerformed(ActionEvent event) { ... } }; Timer t = new Timer(interval, listener); t.start(); }
这种语法的含义是:建立一个实现AcitonListener
接口的类的新对象,须要实现的方法定义在括号内。
一般的语法格式为:
new SuperType(construction parameters) { methods and data }
SuperType
能够是一个接口,也能够是一个类。
因为构造器必需要有一个名字,因此匿名类不能有构造器,取而代之的是:
SuperType
是一个超类时,将构造器参数传递给超类构造器SuperType
是一个接口时,不能有任何构造参数(括号()
仍是要保留的)构造一个类的新对象,和构造一个扩展这个类的匿名内部类的对象的区别:
Person queen = new Person("Mary"); Person count = new Person("Dracula") { ... };
多年来,Java程序员习惯用匿名内部类实现事件监听器和其余回调,现在最好仍是使用lambda表达式,好比:
public void start(int interval, boolean beep) { Timer t = new Timer(interval, event -> { ... }); t.start(); }
可见,用lambda表达式写会简洁得多。
若是想构造一个数组列表,并传递到一个方法:
ArrayList<String> friends = new ArrayList<>(); friends.add("Harry"); friends.add("Tony"); invite(friends);
若是以后都没有再须要这个数组列表,那么最好使用一个匿名列表解决。
invite(new ArrayList<String>() {{ add("Harry"); add("Tony"); }};
注意这里的双括号:
ArrayList
的一个匿名子类有时使用内部类只是为了把一个类隐藏在另外一个类的内部,并不须要内部类引用外围类对象,为此能够将内部类声明static
,取消产生的引用。
编写一个方法同时计算出最大最小值:
double min = Double.POSITIV_INFINITY; double max = Double.NEGATIVE_INFINITY; for(double v : values) { if (min > v) min = v; if (max < v) max = v; }
然而必须返回两个数值,能够顶一个包含两个值的类Pair
:
class Pair { private double first; private double second; public Pair(double f, double s) { first = f; second = s; } public double getFirst() { return first; } public double getSecond() { return second; } }
minmax
方法能够返回一个Pair
类型的对象。
class ArrayAlg { public static Pair minmax(double[] values) { ... return new Pair(min, max); } }
而后调用ArrayAlg.minmax
得到最大最小值:
Pair p = ArrayAlg.minmax(data);
可是Pair
是一个比较大众化的名字,容易出现名字冲突,解决的方法是将Pair
定义为ArrayAlg
的内部公有类,而后用ArrayAlg.Pair
访问它:
ArrayAlg.Pair p = ArrayAlg.minmax(data);
不过与前面的例子不一样,Pair
对象不须要引用任何其余的对象,因此能够把这个内部类声明为static
:
class ArrayAlg { public static class Pair { ... } ... }
只有内部类能够声明为static
,静态内部类的对象除了没有对生成它的外围类对象的引用特权外,其余与全部内部类彻底同样。
在上面的例子中,必须使用静态内部类,这是由于返回的内部类对象是在静态方法minmax
中构造的。
若是没有把Pair
类声明为static
,那么编译器将会给出错误报告:没有可用的隐式ArrayAlg
类型对象初始化内部类对象。
static
和public
类。代理(proxy),这是一种实现任意接口的对象。
利用代理能够在运行时建立一个实现了一组给定接口的新类。
这种功能只有在编译时没法肯定须要实现哪一个接口时才有必要使用。
对于应用程序设计人员来讲,遇到的状况不多,因此先跳过,若是后面有必要再开一个专题进行说明。
我的静态博客: