Interfaces, Lambda Expressions, and InnerClasses
又来写只给自己看到笔记了.每次读一本书读不完,或者读完了又由于没有实践就都忘记是真的很烦,作为这个系列的第一篇,希望可以开一个好头。主要的计划是将原本一本书一个md文件改为一个章节一个文件,也许会有帮助?
6.1 Interfaces
从Java5以来对于
Comparable
有了一些改动,提供了另外一个可选的泛型接口接口的compareTo
函数由原来的接收一个Object
作为参数,变为接收一个泛型T
作为参数,在这种情况下可以免去强制类型转换以及类型判定的繁琐操作1
2
3
4
5
6
7
8
9
10
11public interface Comparable<T>
{
int compareTo(T other);// 参数有了类型T
}
class Employee implements Comparable<Employee> {
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
// 没有cast
}
}当然这也只是可选项,并没有强制要求
对于Comparable,如果比较的是int,double等可能发生溢出的类型的时候(相减的时候),需要使用对应的包装类(boxed class)的
compare
方法来避免溢出。同时理论上,对于大部分的类来说,当
equals
方法返回true
的时候,compareTo
方法也应该返回0,两者在这种情况下应该保持一致性,其中一个特例为BigDecimal
,由于equals
判定的时候考虑了精度理论上Comparable应该实现反向对称性(antisymmetry),即
X.compareTo(Y) = - Y.compareTo(X)
,而继承有时候会破坏这一点(如果使用泛型的Comparable
接口的话)interface
所有的方法自动设置为public abstract
所以没有必要加上访问修饰符(Java 9似乎可以定义private
方法,静态或者类方法,没有深入了解)。同时在实现(implement
)一个接口的时候,所有的对应的方法都必须为public
,与接口对应interface
可以定义方法以及常量(public static final
),但是不能定义成员(instance field)。同时Java8以后为接口内编写函数体提供了可能interface
可以被extends
。与继承不同,一个类可以实现多个接口(implement multiple interfaces),这也是接口与抽象类最大的不同(之一?)。不知道多个接口如果有同样的函数会发生什么。interface
中可以加入static
方法,但是不会继承到实现这个接口的类中。换句话就是接口的static方法只能通过接口名进行调用通过使用
default
关键字可以给接口所定义的函数加上函数体,函数体中可以调用其他的函数1
2
3
4
5public interface Collection
{
int size();
default boolean isEmpty() {return size() == 0;}
}问题:实现一个抽象类的时候一定要实现它所定义的所有方法吗?
并不是,分情况讨论,抽象类实现接口可以不实现所有的方法,同时由
default
修饰的方法也可以不实现(会自动继承),其他情况似乎都要实现(大概)default
的另外一个作用就是提供可维护性,原来写的代码不会因为接口更新之后添加了非default
的方法而无法编译当
dafault
函数之间出现冲突或者与父类的函数发生冲突的时候,两条原则- 父类强于接口
- 接口之间发生冲突必须
Override
,事实上,即使两个接口之间一个通过default
提供了实现,而另一个仅仅只是进行了生命也会发生冲突
有一些类的比较函数我们无法改变,如
String
,这时就需要使用Comparator
,比较的时候实例化一个Comparator
并调用compare
方法即可,也可以传入sort
函数来自定义比较方法(其实有点奇怪compare
为什么不设计成静态方法),当然也可以使用lambda
函数传入sort
一旦为一个类制定了相应的clone策略,那么他的所有子类都必须制定相应的策略,否则有可能出现成员是一个可变对象(mutable)而导致错误的情况(深拷贝).所以有些人建议尽可能少的使用clone,事实上只要不到5%的Java类实现了Cloneable接口.
在Java1.4之前 clone方法只能返回Object对象,但是现在我们可以自定义返回类型从而和类相符合
6.2 Lambda Expressions
例子
1
2
3
4
5
6
7
8
9(String first, String second) -> {
if (first.length() < second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0;
}
// 省略了类型
Comparator<String> comp = (first, second) -> first.length() - second.length();
// 允许函数在某些情况下返回值而其他情况不返回
(int x) -> {if (x >= 0) return 1;}关于接口中的抽象函数和非抽象函数
在Java中lambda表达式只能作为interface(functional interface)的替代,甚至都不能赋值给一个
Object
对象(由于Object
不是一个functional interface
),只能赋值给functional interface函数引用(function reference)
1
2
3var timer = new Timer(1000, event->System.out.println(event));
// 等价于
var timer = new Timer(1000, System.out::println);其中
System.out::println
即为函数引用,函数引用在使用的时候实际上会被编译器转化为对应的interface,并重写(Override)其中唯一的抽象方法(abstract method).可能出现函数本身有重载的情况,由于在使用函数引用的时候并没给出函数签名,而只是给出了函数名. 如此处的println
方法有10个重载方法.此时编译器会根据其要转换的interface中抽象方法的签名来选择合适的函数.(上述例子会选择println(Object))1
2
3
4
5
6Runnable task = System.out::println;
// 其中Runnable接口中有唯一的方法void run(),所以选中的是println()方法
task.run(); // 相当于System.out.println();
// 其他的例子
list.removeIf(Objects::isNull);
// 等价于 list.removeIf(e -> e == null);对于构造函数的引用是
ClassName::new
,对于构造函数的引用可以解决Java本身存在的一些缺陷,如没有泛型数组.new T[n]
会被自动变为new Object[n]
.使用构造函数的引用可以在某种程度上解决这个问题1
2Object[] people = stream.toArray();// 不满足要求
Person[] people = stream.toArray(Person[]:new);lambda
表达式由三个部分组成- 参数列表
- 函数体
- 自由变量(free variable)
自由变量即为lambda表达式可以捕获的外界的变量.Java对其有很严格的要求,只能是
effectively final
(An effetively final variable is a variable that is never assigned a new value after it has been initialized)1
2
3
4
5
6
7
8public static void repeat(String text, int count) {
for (int i = 1; i < count; i++) {
ActionListener listener = event -> {
System.out.println(i + ": " + text);
// 出错,由于引用了i, 但是引用text是可以的
}
}
}lambda
表达式同样有名称冲突的问题.作为lambda表达式参数的名字不能与lambda
表达式所在的代码块含有的其他变量同名this
似乎可以由6来解释,如果在lambda
表达式中使用this
变量的话,指的也是环境中的this
使用
@FunctionalInterface
是一个好主意Comparators
的一些其他的静态方法.comparing
可以用来自定义用于比较的key
,比如将人(Person)根据名字来比较,此时要求用于比较的key
本身是可以比较的(也即实现了Comparable
接口)1
2
3Arrays.sort(people, Comparator.comparing(Person::getName));
// 参数可以是函数引用也可以是lambda表达式
Arrays.sort(people, Comparator.comparing(Person::getName).thenComparing(Person::getFirstName)); // 甚至可以链式制定下一级的比较对象- 基本类型没有实现
Comparable
(甚至都不是类),所以Comparator
有其他的针对基本类型的包装comparing
函数1
Arrays.sort(people, Comparator.comparingInt(p -> p.getName().length()));
6.3 Inner Classes
为什么需要内部类(
Inner Classes
)- 可以向其他的类隐藏内部类的存在
- 内部类可以访问包含者的数据,甚至是private的数据
本节使用以下例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class TalkingClock
{
private int interval;
private boolean beep;
private TalkingClock(int interval, boolean beep) {...}
public void start() {...}
public class TimePrinter implements ActionListener
// an inner class
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}可以看到内部类
TimerPrinter
访问了外部类中的beep
成员,并且为private
,同时我们可将内部类定义为private
,这样的话就只有外部类可以创建并访问内部类,一般来说一个类都只能是public
或者包访问权限,内部类是个例外内部类访问外部类的方法
内部类访问外部类的正确姿势应该是如下:1
2
3
4
5public void actionPerformed(ActionEvent event)
{
...
if (TalkingClock.this.beep) Toolkit.getDefautlToolkit().beep();
}也即通过
OuterClass.this
来访问外部类创建内部类
在外部类之中,创建内部类使用如下语法outerObject.new InnerClass(construction parameters)
1
ActionListener listener = this.new TimrPrinter(); // this.可以去掉
而当内部类被声明为
public
,并且从外部访问时(指外部类的外部),就需要明确指出outerObject
1
2var jabberer = new TalkingClock(1000, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();内部类的静态成员必须为final,并用常量初始化,同时内部类不能有静态方法
同时需要认识到内部类是在范围内产生影响,对于JVM来说根本不会意识到内部类的存在,编译器在编译阶段会将内部类转化为普通的类. 同时会通过一些方法来使得这个类能够访问外部类的私有属性,总之就是让其具有更高的访问权限.也正是由于这个原因,理论上是有办法通过内部类来破除外部类的访问限制的,但是仅仅从代码层面无法做到,需要对.class
文件进行修改,所以一般不考虑本地内部类(
Local Inner Classes
)
就是在类方法中定义的内部类,这种内部类没有访问权限控制,他们只能够在所属的方法中被访问,但是同时,它不仅能够访问外部类,而且可以访问该方法中的局部变量(只能够是effectively final
)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public start()
{
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is "
+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
var listener = new TimePrinter();
var timer = new Timer(interval, listener);
timer.start();
}实际上在1秒之后,局部变量
beep
已经不存在了,所以在内部类中访问的beep
实际上是对原有的beep
进行了复制(是只有基本类型是这样还是类也是这样呢?)