java核心技术-I-6-接口、lambda表达式与内部类

接口

接口的概念

接口不是类,而是希望符合这个接口的类的一组要求。接口不能实例化。

注意:

  1. 接口中方法会默认指定为public abstract
  2. 接口中的方法可以有实现,需加default关键字,使其作为该方法的默认实现(jdk1.8)。
  3. 接口中有静态方法和方法体(jdk1.8)。
  4. 接口中允许将方法定义为 private,使得某些复用的代码不会把方法暴露出去。(jdk1.9)。
  5. 接口中的变量会被默认指定为public static final。(且只能为publicprivate会报错)。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Minterface {
int mm = 123;

int description();

default void id() {
}

public static void name() {

}

private int phone() {
return 0;
}
}

接口的属性

  1. 尽管不能构造接口的对象,却能声明接口的变量。且接口的变量必须引用实现了这个接口的类对象。(这也是实现解耦的关键)

  2. 接口也允许扩展,通过extends关键词来实现扩展(其中的静态变量也会被继承)。

    1
    2
    3
    public interface A{}

    public interface B extends A{}
  3. 一个类可以实现多个接口(而一个类只能继承一个超类)。

接口与抽象类

从概念上来讲,接口是指一个类要满足一些要求;而抽象类,本质上是一个类,即一个种类。

比如Runnable和Thread。前者是一个接口,就是指满足可以run就行了。而Thraed就是指他是一个线程类。

从代码层面上来讲:

  1. 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的。
  2. 接口中不能含有静态代码块,而抽象类是可以有静态代码块。
  3. 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

在一些编程语言中允许一个类继承多个类,如C++,这个特性被称为多继承(multiple inheritance)。Java的设计者选择了不支持多继承,其主要原因是多继承会让语言变得非常复杂,或者效率会降低。

默认方法冲突

接口与超类的冲突

当接口中的默认方法与超类中的方法冲突时,按超类优先的规则来调用方法,即同名的接口中的方法都会被忽略。(注意,只有方法名和参数都相同才会被认为是同一个方法,即函数名和参数类型二者被称为方法签名

接口冲突

当两个接口都实现了getName方法,则编译器会报错,并且要求开发者自己决定选择哪个方法。

可以用以下方式来二选一方法:

1
2
3
4
5
class A implements Inter1, Inter2{
public String getName(){
return Inter1.super.getName();
}
}

lambda表达式

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

(实际上我觉得这是面向对象的函数式编程的补充,比如回调场景要传递一个函数,这时候在Java中,就必须新建一个对象,内部包含一个函数。这明显是浪费的。不如直接传递一个函数,这也是lambda表达式出现的原因吧)

语法

1
2
3
(String i, String j) -> {
return i.length - j.length;
}

注意点如下:

  1. 把这些代码放在{}中,并包含显式的return 语句。

    1
    2
    3
    (String first, String second) -> {
    return first.length() - second-length();
    };
  2. 即使lambda表达式没有参数,也必须要写括号。

    1
    2
    3
    () -> {
    return 0;
    };
  3. 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。

    1
    2
    3
    Comparator<String> comp = (first, second) -> {
    return first.length() - second.length();
    };
  4. 如果该方法只有一个参数,而且这个参数的类型可以推导出,那么可以省略小括号。

    1
    ActionListener listen = event -> System.out.print(event);
  5. 无需指定lambda表达式的返回类型。lambda表达式的返回值类型总是由上下文推导得出。

  6. 当lambda表达式只包含一条语句且其就是返回值,则可以不用加return和花括号,直接写该句即可。

    1
    Comparator<String> comp = (first, second) -> first.length - seconde.length;

函数式接口

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

Java中有很多封装代码的接口,如ActionListenerComparator。lambda表达式与这些接口兼容。

对于需要这些接口的位置,提供一个lambda表达式即可达到同等的效果。

如,(Arrays.sort方法第二个参数需要一个Comparator实例,而Comparator就是一个函数式接口):

1
Arrays.sort(words, (first, second) -> first.length() - second.length());

在底层,Arrays.sort方法会接受实现了Comparator<String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。

实际上,对lambda表达式所能做的也只是转换为函数式接口。

方法引用

有时需要直接调用一个定义好的函数作为lambda表达式体。可以通过以下格式

  1. object.instanceMethod:等价于向方法中传递参数的lambda表达式。
  2. Class.instanceMethod:第一个参数会成为方法的隐式参数,如,String::comparaToIgnoreCase等同于(x, y) -> x.comparaToIgnoreCase(y)
  3. Class.staticMethod:所有参数都传递到静态方法,如:Math::pow等价于(x, y) -> Math.pow(x, y)

来作为该方法的引用。例如:

1
var time = new Time(1000, event -> System.out::println)

同样的,方法引用也不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对应的对象。

注意:

  1. 只有当lambda表达式的体只有调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
  2. 也可以在方法引用中使用this参数。如this::equals等同于x->this.equals(x)
  3. 也可以使用super表达式来引用super中的方法,,如super.equals

构造器引用

构造器引用与方法引用很类似,只不过方法名为new。如,Person::newPerson构造器的一个引用。具体选择哪一个引用取决上下文。

例如:

1
2
3
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());

Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。如:new T[n]会产生错误,因为这会改为new Object[n]

但是我们可以使用Stream.toArray()来获得一个数组,如果不传参,则默认返回Object[]

但是可以如果传入对象的构造器引用,则可以获得对应的数组。

1
Person[] people = stream.toArray(Person[]::new);

变量作用域

在lambda表达式中,可以引用lambda表达式外部的变量。从而形成闭包(enclosure)。但是其由如下特点:

  1. 引用的外部变量只能读取,不能改变。
  2. 在lambda表达式中引用的变量,在外部也不能别改变。

简而言之,lambda表达式中捕获的变量必须实际上最终变量(effective final)。即这个变量初始化之后就不会再为它赋新值。

其还有如下特点:

  1. lambda表达式中声明的变量名不能为外部已经声明的变量名。
  2. 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)是定义在另一个类中的类。其存在原因主要有以下:

  1. 内部类可以对同一个包中的其他类隐藏。
  2. 内部类的方法可以访问外部类的作用域中的数据,包括原本私有的数据。

内部类原来是用来简洁的实现回调,但现在lambda表达式是更好的选择。

内部类的特殊语法规则

我们可以通过OuterClass.this的语法来引用外部对象。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Outer{
private int id;

public class Inner{
pirvate String name;

public int getId(){
return id;
}
}

public Inner getInner(){
return new Inner();
}
}

Outer o = new Outer();
Outer.Inner i = o.getInner(); //这样引用内部类
i.getId(); //null

同样的,可以像上面代码中的Outer.Inner来从外部引用内部类。

内部类的底层

内部类实际上是编译器的工作,而不是虚拟机的工作。所以实际上编译器会把内部类转换为常规的类文件,用$分隔外部类名与内部类名,而虚拟机并不知晓。

例如,Outer$Inner表示其内部类。

实际上在内部类中,会生成一个this$0来表示外部引用。

而既然在虚拟机中内部类被分为两个类,又是如何做到访问外部类的私有成员呢。

其原因在于,编译器向外围类中添加了静态方法access$0之类的方法。它将返回作为参数传递的那个对象的字段。(具体名字取决于编译器。)

此时在内部类中调用外部的成员时。如:

1
if(id)

会被翻译为:

1
if(Outer.access$0(outer))

例如下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
package general;

public class Outer {
private int id = 1;

public class Innter{
public int getId() {
return id;
}
}
}

被编译后会生成两个文件:

  • Outer.class
  • Outer$Innter.class

其代码分别如下:

Outer.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package general;

public class Outer {
private int id = 1;

public Outer() {
}

public class Innter {
public Innter() {
}

public int getId() {
return Outer.this.id;
}
}
}

Outer$Innter.class

1
2
3
4
5
6
7
8
9
10
11
package general;

public class Outer$Innter {
public Outer$Innter(Outer var1) {
this.this$0 = var1;
}

public int getId() {
return this.this$0.id;
}
}

局部内部类

局部内部类是指在一个方法中局部的定义这个类。如:

1
2
3
4
5
6
7
8
9
public int sum(int a, int b){
class BiSUM{
public int sum(int newA, int newB){
return newA + newB;
}
}
BiSUM bs = new BiSUM();
return bs.sum(a, b);
}

注意:

  1. 局部类时不能拥有访问符(即publicprivate
  2. 局部类的作用域仅限于在声明这个类的块中。
  3. 局部类可以做到完全隐藏,除了方法,连兄弟内部类都不知道。

局部内部类不仅可以访问外部类的字段,还可以访问方法的局部变量。但是,与lambda表达式类似,这些局部变量必须是最终变量。即,他们一旦赋值就绝不会改变。

同样的,编译器会在内部类中定义引用的局部变量的复制。如:

1
2
3
4
5
6
7
8
9
10
11
public int sum(int a, int b){
String name = "123";
class BiSUM{
public int sum(int newA, int newB){
System.out.print(name);
return newA + newB;
}
}
BiSUM bs = new BiSUM();
return bs.sum(a, b);
}

会被翻译为如下:

1
2
3
4
public Outer$Inner{
final Outer this$0;
final val$name;
}

匿名内部类

使用局部内部类时,通常还可以省略类名。这样的类被称为内部匿名类。

语法如下:

1
2
3
new SuperType(construction parameters){
inner class method and data
}

superType可以是接口,如ActionListener,这样就是扩展这个接口。

也可以是类,如果是这样,内部类就要扩展这个类。

由于匿名内部类没有名字,所以也没有构造器。如果需要初始化,则可以加一个对象初始化块:

1
2
3
var count = new Person("Tim"){
{initialization}
}

这里使用最多的实际上是一种我们常用的一种编程trick,即新建List并向其中初始化元素:

1
2
3
4
5
ArrayList<Integer> arr = new ArrayList(){{
add(1);
add(2);
add(3);
}};

静态内部类

有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以将内部类声明为static,这样就不会生成那个引用。

注意:

  1. 并且只有内部类可以声明为static。
  2. 与常规类不同,静态内部类可以有静态字段和方法。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Draw{
public static class Pair{
private int x;
private int y;

public void Pair(int x, int y){
this.x = x;
this.y = y;
}
}

public static Pair getPair(int x, int y){
return new Pair(x, y);
}
}

Draw.Euclidean euclidean = Math.getPair(1, 2);

这种形式其实更像是类的命名空间,相当于一个类的集合。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :