接口
接口的概念
接口不是类,而是希望符合这个接口的类的一组要求。接口不能实例化。
注意:
- 接口中方法会默认指定为
public abstract
。 - 接口中的方法可以有实现,需加
default
关键字,使其作为该方法的默认实现(jdk1.8)。 - 接口中有静态方法和方法体(jdk1.8)。
- 接口中允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。(jdk1.9)。
- 接口中的变量会被默认指定为
public static final
。(且只能为public
,private
会报错)。
例如:
1 | public interface Minterface { |
接口的属性
尽管不能构造接口的对象,却能声明接口的变量。且接口的变量必须引用实现了这个接口的类对象。(这也是实现解耦的关键)
接口也允许扩展,通过
extends
关键词来实现扩展(其中的静态变量也会被继承)。1
2
3public interface A{}
public interface B extends A{}一个类可以实现多个接口(而一个类只能继承一个超类)。
接口与抽象类
从概念上来讲,接口是指一个类要满足一些要求;而抽象类,本质上是一个类,即一个种类。
比如Runnable和Thread。前者是一个接口,就是指满足可以run就行了。而Thraed就是指他是一个线程类。
从代码层面上来讲:
- 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
- 接口中不能含有静态代码块,而抽象类是可以有静态代码块。
- 一个类只能继承一个抽象类,而一个类却可以实现多个接口。
在一些编程语言中允许一个类继承多个类,如C++,这个特性被称为多继承(multiple inheritance)。Java的设计者选择了不支持多继承,其主要原因是多继承会让语言变得非常复杂,或者效率会降低。
默认方法冲突
接口与超类的冲突
当接口中的默认方法与超类中的方法冲突时,按超类优先的规则来调用方法,即同名的接口中的方法都会被忽略。(注意,只有方法名和参数都相同才会被认为是同一个方法,即函数名和参数类型二者被称为方法签名)
接口冲突
当两个接口都实现了getName
方法,则编译器会报错,并且要求开发者自己决定选择哪个方法。
可以用以下方式来二选一方法:
1 | class A implements Inter1, Inter2{ |
lambda表达式
lambda表达式是一个可传递的代码块,可以执行多次或一次。
(实际上我觉得这是面向对象的函数式编程的补充,比如回调场景要传递一个函数,这时候在Java中,就必须新建一个对象,内部包含一个函数。这明显是浪费的。不如直接传递一个函数,这也是lambda表达式出现的原因吧)
语法
1 | (String i, String j) -> { |
注意点如下:
把这些代码放在{}中,并包含显式的return 语句。
1
2
3(String first, String second) -> {
return first.length() - second-length();
};即使lambda表达式没有参数,也必须要写括号。
1
2
3() -> {
return 0;
};如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
1
2
3Comparator<String> comp = (first, second) -> {
return first.length() - second.length();
};如果该方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号。
1
ActionListener listen = event -> System.out.print(event);
无需指定lambda表达式的返回类型。lambda表达式的返回值类型总是由上下文推导得出。
当lambda表达式只包含一条语句且其就是返回值,则可以不用加
return
和花括号,直接写该句即可。1
Comparator<String> comp = (first, second) -> first.length - seconde.length;
函数式接口
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。
Java中有很多封装代码的接口,如ActionListener
或Comparator
。lambda表达式与这些接口兼容。
对于需要这些接口的位置,提供一个lambda表达式即可达到同等的效果。
如,(Arrays.sort
方法第二个参数需要一个Comparator
实例,而Comparator
就是一个函数式接口):
1 | Arrays.sort(words, (first, second) -> first.length() - second.length()); |
在底层,Arrays.sort
方法会接受实现了Comparator<String>
的某个类的对象。在这个对象上调用compare
方法会执行这个lambda表达式的体。
实际上,对lambda表达式所能做的也只是转换为函数式接口。
方法引用
有时需要直接调用一个定义好的函数作为lambda表达式体。可以通过以下格式
object.instanceMethod
:等价于向方法中传递参数的lambda表达式。Class.instanceMethod
:第一个参数会成为方法的隐式参数,如,String::comparaToIgnoreCase
等同于(x, y) -> x.comparaToIgnoreCase(y)
。Class.staticMethod
:所有参数都传递到静态方法,如:Math::pow
等价于(x, y) -> Math.pow(x, y)
。
来作为该方法的引用。例如:
1 | var time = new Time(1000, event -> System.out::println) |
同样的,方法引用也不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对应的对象。
注意:
- 只有当lambda表达式的体只有调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
- 也可以在方法引用中使用this参数。如
this::equals
等同于x->this.equals(x)
。 - 也可以使用super表达式来引用super中的方法,,如
super.equals
。
构造器引用
构造器引用与方法引用很类似,只不过方法名为new
。如,Person::new
是Person
构造器的一个引用。具体选择哪一个引用取决上下文。
例如:
1 | ArrayList<String> names = ...; |
Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。如:new T[n]
会产生错误,因为这会改为new Object[n]
。
但是我们可以使用Stream.toArray()
来获得一个数组,如果不传参,则默认返回Object[]
。
但是可以如果传入对象的构造器引用,则可以获得对应的数组。
1 | Person[] people = stream.toArray(Person[]::new); |
变量作用域
在lambda表达式中,可以引用lambda表达式外部的变量。从而形成闭包(enclosure)。但是其由如下特点:
- 引用的外部变量只能读取,不能改变。
- 在lambda表达式中引用的变量,在外部也不能别改变。
简而言之,lambda表达式中捕获的变量必须实际上最终变量(effective final)。即这个变量初始化之后就不会再为它赋新值。
其还有如下特点:
- lambda表达式中声明的变量名不能为外部已经声明的变量名。
- lambda表达式中的this,指向创建这个表达式的方法的this参数。
java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的部分函数式接口有:
序号 | 接口 & 描述 |
1 | BiConsumer<T,U> 代表了一个接受两个输入参数的操作,并且不返回任何结果 |
2 | BiFunction<T,U,R> 代表了一个接受两个输入参数的方法,并且返回一个结果 |
3 | BinaryOperator<T> 代表了一个作用于于两个同类型操作符的操作,并且返回了操作符同类型的结果 |
4 | BiPredicate<T,U> 代表了一个两个参数的boolean值方法 |
5 | BooleanSupplier 代表了boolean值结果的提供方 |
6 | Consumer<T> 代表了接受一个输入参数并且无返回的操作 |
7 | DoubleBinaryOperator 代表了作用于两个double值操作符的操作,并且返回了一个double值的结果。 |
8 | DoubleConsumer 代表一个接受double值参数的操作,并且不返回结果。 |
9 | DoubleFunction<R> 代表接受一个double值参数的方法,并且返回结果 |
内部类
内部类(inner class)是定义在另一个类中的类。其存在原因主要有以下:
- 内部类可以对同一个包中的其他类隐藏。
- 内部类的方法可以访问外部类的作用域中的数据,包括原本私有的数据。
内部类原来是用来简洁的实现回调,但现在lambda表达式是更好的选择。
内部类的特殊语法规则
我们可以通过OuterClass.this
的语法来引用外部对象。如:
1 | public class Outer{ |
同样的,可以像上面代码中的Outer.Inner
来从外部引用内部类。
内部类的底层
内部类实际上是编译器的工作,而不是虚拟机的工作。所以实际上编译器会把内部类转换为常规的类文件,用$分隔外部类名与内部类名,而虚拟机并不知晓。
例如,Outer$Inner
表示其内部类。
实际上在内部类中,会生成一个this$0
来表示外部引用。
而既然在虚拟机中内部类被分为两个类,又是如何做到访问外部类的私有成员呢。
其原因在于,编译器向外围类中添加了静态方法access$0
之类的方法。它将返回作为参数传递的那个对象的字段。(具体名字取决于编译器。)
此时在内部类中调用外部的成员时。如:
1 | if(id) |
会被翻译为:
1 | if(Outer.access$0(outer)) |
例如下面的例子:
1 | package general; |
被编译后会生成两个文件:
- Outer.class
- Outer$Innter.class
其代码分别如下:
Outer.class
1 | package general; |
Outer$Innter.class
1 | package general; |
局部内部类
局部内部类是指在一个方法中局部的定义这个类。如:
1 | public int sum(int a, int b){ |
注意:
- 局部类时不能拥有访问符(即
public
和private
) - 局部类的作用域仅限于在声明这个类的块中。
- 局部类可以做到完全隐藏,除了方法,连兄弟内部类都不知道。
局部内部类不仅可以访问外部类的字段,还可以访问方法的局部变量。但是,与lambda表达式类似,这些局部变量必须是最终变量。即,他们一旦赋值就绝不会改变。
同样的,编译器会在内部类中定义引用的局部变量的复制。如:
1 | public int sum(int a, int b){ |
会被翻译为如下:
1 | public Outer$Inner{ |
匿名内部类
使用局部内部类时,通常还可以省略类名。这样的类被称为内部匿名类。
语法如下:
1 | new SuperType(construction parameters){ |
superType可以是接口,如ActionListener
,这样就是扩展这个接口。
也可以是类,如果是这样,内部类就要扩展这个类。
由于匿名内部类没有名字,所以也没有构造器。如果需要初始化,则可以加一个对象初始化块:
1 | var count = new Person("Tim"){ |
这里使用最多的实际上是一种我们常用的一种编程trick,即新建List并向其中初始化元素:
1 | ArrayList<Integer> arr = new ArrayList(){{ |
静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以将内部类声明为static,这样就不会生成那个引用。
注意:
- 并且只有内部类可以声明为static。
- 与常规类不同,静态内部类可以有静态字段和方法。
示例:
1 | public class Draw{ |
这种形式其实更像是类的命名空间,相当于一个类的集合。