【Core Java】06 Java 接口和内部类

接口和内部类

接口

接口

我们常说,“接口是多个类的公有规范”,这句话本没错,但我们容易误解或忽略"规范"这两个字,这会令初学者远离接口的本质。

如果不理解接口,也就很难理解"规范",从而再次很难理解接口,所以我们应该使用更加浅显的话来描述接口 —— “接口是对类的需求描述”

接口是需求描述,对类的需求描述。

编写接口的目的是,为了完成一些通用的功能,需要向未知类索取一些方法,这些方法就是对类的需求描述,然后我们对这些行为进行抽象,定义这些抽象行为的规范。

相反,实现接口是为了让我们的类更加通用,与他人的类进行通信和对接,实现更强大的功能。

一个类最好不要依赖任何其他类的细节 —— 这样低耦合的设计很棒,但也带来了很多问题,例如类之间的通信问题。

类的封装性带来了维护上的便利,但同时也阻碍了实现功能的灵活性。例如,如果代码可以随意耦合,即便维护时会吐血,但我们可以轻松实现某个类对另一个类实例的任何行为。

接口就是对这种情况的妥协,不想放弃低耦合的封装性设计,但也想获得实现功能的灵活性。

当一个类遵循了一个接口的规范,由于规范是可见的,此时任意其他类就可以通过接口与实现类进行通信和交互。

接口和抽象类

抽象类是多个类的公有部分,它的继承设计一定是从上至下的,而接口的实现更像是平行关系,这一点可以从标准库里的接口命名中看出一些端倪。

例如ComparableCloneableIterable这样的形容词,或者是CollectionListenerComparator这样的描述功能的词,而抽象类的命名则大概是PersonEmployee这样的名词。

抽象类是多个类的抽象概念,而接口则意味着某个类应该具有的功能。

我们来解释一下:

当我们说一个类继承自某个抽象类,例如Student继承自Person,我们讲"学生是一个人"。

而当我们实现了某个接口,例如Person实现了Comparable,我们则描述为"人是具有比较功能的"或"人是可比较的"。

也就是说,接口抽象的是行为,与实现该接口的类没什么太大的关系,所以从概念上讲,接口很难向抽象类靠拢。

甚至,为了区分概念,Java 标准也做足了功夫,我们说**“如非必要勿增实体”**,然而却不惜为这种关系多造了一个关键字implements,而不是使用extends。由此可见,在 Java 设计者眼里,这种区分很有必要。

此外,接口与抽象类最大的区别还在于继承关系:接口本身允许多重继承,且类允许多重实现接口,而类的继承仅允许多层继承。

为什么接口允许多重继承?

首先,类之间的继承关系也可以允许多重继承,只不过会很复杂,所以 Java 放弃了这一点,关于多重继承和多层继承的讨论,见笔者的文章:继承和多态

虽然多重继承很复杂,但也同时带来一些设计上的便利性。回到接口,既然接口描述了一组对类的功能需求,那么某个类可能具有多个功能,例如某个类是可比较的,也是可克隆的,甚至还可能是一个监听器,这并不冲突。

接口特性

接口的定义风格与类差不多:

interface Comparable {
    
    
    int compareTo(Object o);
}

接口不是类,但它类似类,类能干的事它几乎都能干,它也有封装、继承和多态性。

  • 接口更像抽象类(相对来讲),它无法实例化。

  • 接口具有多态性,接口的主要功能就是通过多态实现的。

  • 接口可以多重继承。

  • 接口方法冲突遵循类优先原则。

  • 接口可以包含抽象方法、静态常量域、静态方法(Java 8)、默认方法(Java 8)和私有方法(Java 9)。

我们先说最后一点 —— 接口中允许包含的内容,以及为什么运行这样做。

抽象方法和静态常量域

根据接口最初的设计思想,它里面没必要存在私有方法,因为私有方法无法被实现。接口中一般会声明一些公共的方法,以便某个类去实现它。

所以,接口中的方法都会隐式被修饰为public

接口中还允许定义静态常量域,所有域将被隐式修饰为public static final,没什么理由,毕竟没有任何理由认为这是不合法的。

静态方法

在 Java 8 中,接口允许定义静态方法。

在 Java 标准库中存在数量众多成对出现的接口和针对该接口的工具类,当我们允许在接口中定义静态方法,也就不再需要额外定义一个工具类了。

默认方法

在 Java 8 中,接口允许定义默认方法,即为某个抽象方法提供默认实现。

使用default关键字。

interface Listener {
    
    
    default void event1 (Object e) {
    
    
        // do sth
    };
    default void event1 (Object e) {
    
    
        // do sth
    };
}

首先,默认方法可以简化代码,或调试过程。

想象一下,某个事件回调接口中存在 20 个抽象方法,如果我只想要简单调试一下某个事件,却需要实现 20 个方法,太麻烦了,不是么?

接口的设计者可以为所有方法提供一个空实现,当实现该接口时,开发者仅需要实现他们真正关注的方法即可。

第二个好处,被称为接口演化(interface evolution)(也称接口升级)。

当我们为一个使用了很久的接口添加新方法后,若重新编译所有相关类,所有已实现该接口的类将全部变为抽象类 —— 因为它们都没有实现该方法,未完全实现抽象方法的类将是一个抽象类。但如果为这个新方法提供了默认实现,那么所有实现该接口的类将会自动实现这个方法。

为接口添加默认方法,可以保证现有代码不会出现兼容性问题,这被称为源代码兼容(source compatible)

此外,若添加一个非默认方法,而实现该接口的类被包含在一个 jar 文件中,即该类已经编译为机器码。此时该类仍可以正常运作,但试图调用该方法时将出现AbstractMethodError

为接口添加非默认方法,可以保证已编译过的类正常运作,这被称为二进制兼容

私有方法

在 Java 9 中,接口中允许定义私有方法。

乍一看让人无法理解,有什么用呢?私有方法又不能被实现。

实际上,私有方法是为默认方法服务的,如果某个默认方法涉及到许多过程,全部写在一个函数 —— 实在太想使用"函数"这个词了,尽管 Java 里不存在函数 —— 里将非常冗余。这时候就可以抽象几个过程,作为私有方法。

方法冲突

一个类可以继承多个接口,还能同时继承一个类,毕竟接口涉及到了多重继承,那就必须面对这个问题。

如何解决呢?Java 给出的规则很简单 —— 类优先

  • 父类方法与接口默认方法冲突

    类优先就是如此,在这种情况下,接口的方法将被忽略。毕竟父类已经实现了这个行为,没有理由再使用接口的默认方法。

  • 多个接口的方法冲突

    此时面临两个选择,实现或不实现,前者不用多说,后者会导致该类成为一个抽象类,毕竟它没有实现所有的抽象方法。

  • 多个接口的默认方法冲突

    只要至少存在一个冲突的方法是具有默认实现的,Java 就要求开发者手动来解决这个多义性,选择一个默认实现:

    class Person implements Named, ...{
          
          
        // ...
        @Override
        String getName() {
          
          
            return Named.super.getName();
        }
    }
    

    或手动实现:

    class Person implements Named, ...{
          
          
        // ...
        String name;
        @Override
        String getName() {
          
          
            return this.name;
        }
    }
    

常见接口

Comparable 接口

实现接口

Comparable接口中仅包含一个抽象方法:

实现该接口就意味着,实现类的实例均可比较。

public interface Comparable<T> {
    
    
    public int compareTo(T o);
}

看到这里,读者的脑海中可能不自觉的就蹦出来两个字"排序"。的确如此,当我们实现了这个接口后,就可以调用 Arrays 下的静态方法 sort 为数组排序了。

Comparable接口为实现类和Arrays.sort方法之间提供了一条沟通的渠道,这很像 C/C++ 中 algorithm 头文件中声明的 sort 函数,不是吗?

sort(arr.begin(), arr.end(), [](auto l, auto r){
    
     return l < r;});

接口中还有一个没有明确说明的附加语义:

当调用x.compareTo(y)时,这个compareTo方法的语义为:x - y,即当 x 小于 y 时,返回一个负数。当 x 等于 y 时,返回 0。当 x > y 时,返回一个正数。这是一个统一的口头协定,与任何实现无关。

下面我们来实现这个接口,实现接口需要使用implements关键字来代替extends

例如,我们有一个代表人的Person类:

class Person {
    
    
    private int age;
    private String name;
    
    public int getAge() {
    
    return this.age;}
    public String getName() {
    
    return this.name;}
    
    Person(int age, String name) {
    
    
        this.age = age; this.name = name;
    }
}

若以每个人的年龄为比较依据,可以这样实现:

class Person implements Comparable<Person> {
    
    
    private int age;
    private String name;
    
    public int getAge() {
    
    return this.age;}
    public String getName() {
    
    return this.name;}
    
    Person(int age, String name) {
    
    
        this.age = age; this.name = name;
    }
    
    @Override
    public int compareTo(Person obj) {
    
    
        return this.age - obj.age;
    }
}

继承中出现的问题

如果详细研究过Object类的equals方法,应该会意识到一个问题,即在发生多态时,compareTo的语义问题。

equals 相关讨论请移步 -> Object类的equals方法讨论

equals类似,Java 语言标准规定了compareTo应遵循反对称原则:对于任意的对象 x 和 y,应该满足sgn(x.compareTo(y)) = -sgn (y.compareTo(x)),即调换调用者和参数后,结果的符号应该相反。

发生多态时,尤其需要注意这一点。

如果Person的子类Student重写了该方法,那就要做好PersonStudent实例相比较的准备。

参考Object类的equals方法讨论,若父类与子类各自比较的语义不同,即子类拥有单独的比较语义,则应该让子类重写方法,并在每个compareTo的实现中提前使用getClass检测类型是否一致。

若父子类各自比较的语义相同,则应在父类中使用final修饰该方法,不允许子类重写它。

此外,若不论年龄大小都想让Teacher的实例大于Student的实例,可以在父类Person中实现一个类似等级的方法,例如称之为level,每个子类都重写该方法,然后考虑在父类中实现一个依赖levelcompareTo方法。

Comparator

该接口用于定义临时的排序比较规则。

例如Array.sort的一个重载形式,第一个参数是数组,第二个参数就是一个Comparator实例。

对于第二个参数,传入一个实现了该接口的对象即可。

Arrays.sort(arr, new Comparator<Integer>() {
    
    
    @Override
    public int compare(Integer o1, Integer o2) {
    
    
        return o1 - o2;
    }
});

Cloneable

在顶级类 Object 中存在于一个protected方法clone。该方法返回另一个实例,其中域的值将与调用实例域的值完全相同。

这意味着,clone仅提供一层拷贝。更深层的可变引用类型域的拷贝,应该由开发者手动重写实现。

"可变引用类型"指实例状态可更改的类型,例如 Date,对应地,不可变引用类型指实例状态不可更改的类型,例如 String、LocalDate。

在调用clone时,前者作为域必须被考虑,否则这个拷贝就是不安全的,而后者既然不可变,也就可以不被考虑。

不过由于可见性的问题,我们必须重写该方法,才能在类外调用clone

此外,我们还需要实现Cloneable接口,否则当试图调用clone时将抛出CloneNotSupportedException异常。

class Type implements Cloneable{
    
    
    @Override
    public Type clone() throws CloneNotSupportedException {
    
    
        // 拷贝自身
        Type cloned = super.clone();
        
        // 拷贝实例域
        cloned.ref = ref.clone();
        return cloned;
    }
}

Cloneable接口中不包含任何内容,它是 Java 提供的一组标记接口 (tagging interface) 或称记号接口 (marker interface) 之一,这个接口只是作为一个标记,指示类的设计者了解克隆过程。

还要注意一点,一旦父类实现了公有的clone方法,那么就可以在任何位置拷贝其子类的对象,而其子类甚至没有选择的余地,因为继承不允许降低可见性。

最后,所有数组都有 public 的clone方法。

lambda

函数式接口

函数式接口(functional interface),指仅拥有一个抽象方法的接口。

Java 中没有函数的概念,但在很多时候,传递一个接口的实现类实例,显然不如传递一个函数指针那样简洁和直观。

例如,为Arrays.sort传递一个数组及一个Comparator的实现类。我们需要让一个类实现该接口,然后实例化一个对象,最后将其作为参数传入Arrays.sort中。

很显然,这个过程就是为了传递一个函数的调用,所以这种接口被称为函数式接口。

最初,我们通过这种风格的代码来简化这个过程:

这也称匿名局部内部类,马上就会提到。

Arrays.sort(arr, new Comparator<Person>() {
    
    
    @Override
    public int compare(Person l, Person r) {
    
    
        return l.age - r.age;
    }
});

这种写法表示创建一个该类的匿名子类并实例化,被重写的方法写在紧跟着的大括号中。

对于接口也是如此,new 接口,在其后紧跟一对大括号表示继承,即创建一个匿名类的同时实现该接口。然后在大括号内重写接口中的方法。

此外,关于函数式接口,还有一个接口注解@FunctionalInterface,用于在编译期检查该接口是否是一个函数式接口。

lambda

很显然,开发者并不满足于此,因为 lambda 表达式出现了。

Arrays.sort(arr, (Person l, Person r) -> l.age - r.age);

对比一下 C++ 和 JS:

sort(arr.begin(), arr.end(), [](auto l, auto r){
    
    
    return l - r; // 令人又爱又恨的运算符重载
});
arr.sort((l,r) => l - r);

C++ 的 lambda 实在太难看,能用到的括号居然全用上了。

接下来为上面 Java 的 lambda 换几种写法,体会一下它的特性和风格:

Arrays.sort(arr, (Person l, Person r) -> {
    
    
    return l.age - r.age;
});
Arrays.sort(arr, (l, r) -> l.age - r.age);

Java lambda 的参数列表会自动推断类型,而对于单一参数的 lambda,可以像 JS 那样去掉括号,此时仅能通过自动类型推断为唯一的参数指定类型。

event -> {
    
    
    // solve sth
}

不过,Java 并没有引入更多的内容,lambda 实际上还是一个接口(更像是一个语法糖),可以将它理解为任意函数式接口的子实现类。

Comparator<Person> comp = (l, r) -> l.age - r.age;

值得注意的是,lambda 仅能转型为函数式接口,而不能是其他的什么东西,Object 也不行。

方法引用

我们知道,Java 中的"函数指针"可以通过反射对象 Method 实现,或者也可以将方法包装在类里来实现(即函数式接口)。

此外,我们还有几种方法来代替他们,第一种被称为方法引用。

考虑一个定时器(类似 JS 中的setInterval):

new Timer(1000, event -> System.out.println(event)).start();

可以改写为:

new Time(1000, System.out::println).start;

后者的System.out::println就是方法引用,与前者传入的 lambda 等价。

联想一下,应该能猜到,方法引用应该有三种形式:

  • 第一种是实例::普通方法的形式

    就像上边的例子一样:System.out::println等价于event -> System.out.println(event)

    实例::普通方法中的普通方法将由该实例调用。

    再举一个清晰的例子:

    personList.removeIf(personObj::equals);
    

    等价于:

    personList.removeIf(obj -> personObj.equals(obj));
    

    此外,该形式的方法引用的实例部分,允许使用thissuper,分别代表在该类作用域下的方法引用。

  • 第二种是类名::普通方法的形式

    该形式与第一种形式的不同之处在于,它缺少一个调用者。

    例如Person::equals,如果这样等价obj -> ?.equals(obj)显然不行。

    该方法引用将等价于(obj1, obj2) -> obj1.equals(obj2),它将以第一个参数作为调用者

  • 第三种是类名::静态方法的形式

    这种形式没什么好说的,静态方法属于类,与实例无关,所以有几个参数就对应到几个参数的 lambda 实例。

构造器引用

构造器引用,就是将构造器作为函数式接口,该接口将返回一个新对象。形式如 -> 类名::new

举个例子。

在 JS 中经常使用的map方法,在 Java 中也有类似的东西。

Java 的map方法属于 Stream 类,而实现了Collection接口的类实例,都可以通过stream方法来得到对应的 Stream 实例,从而调用该方法。详细内容可以查阅 api 文档,我们以后也会讨论它。

简单来讲,map会对每一个元素调用一次我们提供的callback,然后作为当前元素的新值。

ArrayList<Person> arr = new String[]{
    
    "小明", "高厉害",/*...*/}.stream().map(Person::new);

toArray 方法也允许这样做:

Person arr[] = new String[]{
    
    "小明", "高厉害",/*...*/}.stream().toArray(Person::new);

作用域

变量捕获

lambda 的作用域就是局部作用域,它可以捕获局部作用域的变量。

public class test {
    
    
    public static void main(String[] args) {
    
    
        int a = 1;
        Runnable closure = () -> {
    
    
            System.out.println(a);
        };
        closure.run();
    }
}

注意:

  • “lambda 的作用域就是局部作用域”,意味着 lambda 表达式中的thissuper等关键字的语义与局部作用域相同。

  • Java 要求:允许被捕获的变量必须是事实上的最终变量 (effectively final)。即被捕获的可以不使用final修饰,但一定未被更改。这主要是为了避开并发下的引用安全问题。

    例如下方示例中的 a 就不是 effectively final 的:

    public class test {
          
          
        public static void main(String[] args) {
          
          
            int a = 1;
            Runnable closure = () -> {
          
          
                System.out.println(a);
            };
            a = 2; // error
            closure.run();
        }
    }
    

闭包

虽然 Java 对被捕获的变量做出了 effectively final 的限制,但它并不怎么影响闭包的灵活性。

演示一个简单的计数器闭包:

public class test {
    
    
    static class Pack<T>{
    
    
        T val;
        public Pack(T aVal){
    
     this.val = aVal; }
    }
    public static Supplier<Integer> getCounter(int startAt) {
    
    
        Pack<Integer> a = new Pack<>(startAt);
        return () -> a.val++;
    }
    public static void main(String[] args) {
    
    
        Supplier<Integer> counter = getCounter(1);
        System.out.println(counter.get());
        System.out.println(counter.get());
    }
}

输出:

1

2

上面的程序在 test 类中嵌套了 Pack 类,这样的类被称为内部类,马上就就要介绍。

原理

Java 是如何捕获变量的?

看下面的示例程序:

public static void main(String[] args) {
    
    
    Integer a = 1;
    Runnable lambda = () -> System.out.println(a);
    lambda.run();
}

输出:

1

使用反射机制查看运行时的 test 类结构:

这里可以使用javap工具来查看,javap -p <类名>

public class top.gaolihai.test.test {
    
    
  public top.gaolihai.test.test();
  public static void main(java.lang.String[]);
  private static void lambda$main$0(java.lang.Integer);
}

从上到下依次是,默认构造、入口方法及编译器为 lambda 生成的静态方法。看上去 lambda 应该是一种编译器行为。

捕获变量的机制,就是将 lambda 引用到的外部变量作为参数传入生成的静态方法中。

我们可以利用反射机制尝试调用这个方法:

(这与我们曾经在 C++ 中,通过虚函数表用指针摸到方法入口地址有异曲同工之妙)

Integer a = 1;
Runnable lambda = () -> System.out.println(a);
Method[] methods  = test.class.getDeclaredMethods();
try {
    
    
    methods[1].invoke(null, 3);
} catch (Exception e) {
    
    
    e.printStackTrace();
}

输出

3

因为"被捕获变量"不过是一个参数而已,我们甚至改变它了的值。

常用函数式接口

常用函数式接口

函数式接口 参数类型 返回类型 抽象方法名 静态方法
Runnable none void run
Supplier none T get
Consumer T void accept andThen
BiConsumer<T, U> T, U void accept andThen
Function<T, R> T R apply compose, andThen, identity
BiFunction<T, U, R> T, U R apply andThen
UnaryOperator T T apply compose, andThen, identity
BinaryOperator T, T T apply andThen, maxBy, minBy
Predicate T boolean test and, or, negate, isEqual
BiPredicate<T, U> T, U boolean test and, or, negate

基本类型函数式接口

基本类型的函数式接口,使用这些接口可以减少自动拆箱和装箱,其中的 PQ 指代 int、float 等基本类型:

函数式接口 参数类型 返回类型 抽象方法名
BooleanSupplier none boolean getAsBoolean
PSupplier none P getAsP
PConsumer P void accept
ObjPConsumer T, P void accept
PFunction P T apply
PToQFunction P Q applyAsQ
ToPFunctioi T P applyAsP
ToPBFunction<T,U> T, U P applyAsP
PUnaryOperator P P applyAsP
PBinaryOperator P,P P applyAsP
PPredicate P boolean test

此外,大多标准函数式接口都提供了非抽象的静态方法,来处理回调逻辑。

例如 Comparator 接口提供了一些值提取器,传入一个方法引用,返回一个函数式接口实现类的实例。更详细的内容可以查阅 api 手册,或者查看源代码。

内部类

Java 允许类的嵌套,定义在另一个类中的类就被称为内部类。

内部类可以分为三种,(成员)内部类、静态内部类、局部内部类,其中局部内部类又存在一种特殊形式,被称为匿名内部类。

  • 内部类
    • 成员内部类
    • 静态内部类
    • 局部内部类
      • 普通局部内部类
      • 匿名局部内部类

C++ 允许类的嵌套,对应到 Java 中,比较类似于静态内部类。

静态内部类与外部类是类与类之间的关系,不涉及类实例,而 Java 中的成员内部类则是实例相关的。

每个实例都可以拥有自己的成员内部类实例,而且该成员内部类实例可以直接访问外层类实例的域和方法。

成员内部类

外部类对内部类的可见性

先感受一下内部类的风格:

public class Person {
    
    
    // Phone 作为内部类
    class Phone {
    
    
        // 访问外部类实例的私有域
        public String getMasterName() {
    
    
            return Person.this.name;
        }
    }
    // 私有域
    private String name;
    private Phone phone;

    // phone 的 getter and setter
    public Phone getPhone() {
    
    
        return phone;
    }
    public void setPhone(Phone phone) {
    
    
        this.phone = phone;
    }

    public Person(String name) {
    
    
        this.name = name;
    }
};

成员内部类是实例相关的,每个外部类的实例都可以拥有一个独立的内部类实例,内部类实例也可以直接访问到外部类实例的所有域(外部类只能访问内部类的公有域)。

例如内部类Phone的方法getMasterName通过外部类名.this的方式拿到了外部类实例的引用:

class Phone {
    
    
    // 访问外部类实例的私有域
    public String getMasterName() {
    
    
        return Person.this.name;
    }
}

反过来,在外部类,使用外部类实例.new 内部类名的方式来调用内部类的构造器:

例如,我们在外部类Person中为内部类Phone的实例化提供一个方法:

public Phone phoneInit(){
    
    
    return this.new Phone();
}

在内部类,外部类实例的部分是多余的,可以省略:

(不过可以通过显示指定的方式为任意外部类实例构造一个内部类实例)

public Phone phoneInit(){
    
    
    return new Phone();
}

但在外部类的作用域之外,想要调用内部类的构造器,必须指定外部类实例部分:

注意,在外部类的作用域外,可以这样引用内部类Outer.Inner

Person person = new Person("高厉害");
Person.Phone phone1 = person.new Phone();

内部类对其他类的可见性

只有内部类可以被声明为私有的。

当被声明为private后, 外部类的作用域外就无法访问它了,就好像外部作用域无法访问私有域一样,私有的内部类仅允许通过外部类来调用构造器、域和方法。

静态域和静态方法

  • 应该注意到一个问题,既然成员内部类是实例相关的,那么每个外部类实例都可以拥有和不拥有内部类的实例,即每个外部类实例都拥有独立的内部类。

    也就是说,这些内部类的静态域可能不是统一的,鉴于此,Java 设计者决定让内部类仅中仅允许声明final的静态域。

  • 内部类中不允许出现静态方法。

    内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态域和方法。显然,Java 设计者认为相对于这种复杂 性来说,它带来的好处有些得不偿失。

实现原理

外部类对内部类的可见性

在 Java 1.8 中,内部类是一种编译器行为,内部类会被编译为独立的类文件,命名通过$分隔外部类与内部类名,虚拟机则对此一无所知。

读到这里应该有很多疑惑,例如,针对内部类而言,外部类的全部可见性是如何实现的?毕竟这是完全独立的两个类,到底怎么做才能让一个访问另一个类的所有域?

这时候肯定会自然的想到 lambda 的捕获变量, lambda 对外部作用域变量的捕获,是通过预留参数位置,传参实现的。

差不多,外部类对内部类的可见性基本上也是这样实现的,但令人诧异的是,仅有引用也无法直接访问私有域(毕竟是编译器的行为)。

考虑下面这段代码:

public class Person {
    
    
    // Phone 作为内部类
    class Phone {
    
    
        
        // 访问外部类实例的私有域
        public String getMasterName() {
    
    
            return Person.this.name;
        }

    }
    // 私有域
    private String name;
    private Phone phone;

    // phone 的 getter and setter
    public Phone getPhone() {
    
    
        return phone;
    }
    public void setPhone(Phone phone) {
    
    
        this.phone = phone;
    }
	
    // 构造
    public Person(String name) {
    
    
        this.name = name;
    }
}

很简单,每个人Person有一个名字private Person.name和一部手机private Person.phone,手机类作为人类的内部类。

手机类有一个方法getMasterName,可以获取其外部类实例的名字private Person.name

// 访问外部类实例的私有域
public String getMasterName() {
    
    
    return Person.this.name;
}

我们说了,在 Java 1.8 中,内部类是一种编译器行为(重要的事情说三遍),那么Person.this这个引用是哪来的?

使用反射,我们来看一下运行时内部类的结构:

class top.gaolihai.test.Person$Phone {
    
    
  	// 外部类实例的引用
    final top.gaolihai.test.Person this$0;
    // 构造器
    top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
    // 访问外部类的方法
    public java.lang.String getMasterName();
}

很清晰,编译器为了引用外部类实例,生成了一个域this$0,而这个域的初始化则是通过为构造额外提供一个参数来实现的。

但是还存在一个问提,内部类实例是如何通过一个引用访问到外部类实例的私有域的?

反编译类文件,得到:

class top.gaolihai.test.Person$Phone {
    
    
  final top.gaolihai.test.Person this$0;

  public java.lang.String getMasterName();
    Code:
       0: aload_0
       1: getfield      #2                  // Field this$0:Ltop/gaolihai/test/Person;
       4: invokestatic  #4                  // Method top/gaolihai/test/Person.access$000:(Ltop/gaolihai/test/Person;)Ljava/lang/String;
       7: areturn

  top.gaolihai.test.Person$Phone(top.gaolihai.test.Person, top.gaolihai.test.Person$1);
    Code:
       0: aload_0
       1: aload_1
       2: invokespecial #1                  // Method "<init>":(Ltop/gaolihai/test/Person;)V
       5: return
}

可以使用 Java 提供的工具 javap 对类文件进行反编译,javap -c <类名>

getMasterName方法的反编译结果中,第七和第八行,先取到了外部类实例的引用getfield,然后调用了一个位于外部类静态方法Person.access$000

看来编译器对外部类也动了手脚,那么我们再通过反射看一下外部类Person的结构:

public class top.gaolihai.test.Person {
    
    
  	// 私有域
    private java.lang.String name;
    private top.gaolihai.test.Person$Phone phone;
    // getter 和 setter
    public top.gaolihai.test.Person$Phone getPhone();
    public void setPhone(top.gaolihai.test.Person$Phone);
    // 构造
    public top.gaolihai.test.Person(java.lang.String);
    static java.lang.String access$000(top.gaolihai.test.Person);
}

最后一行,编译器为外部类添加了一个access$000的静态方法,不难看出,该方法类似反射对象Fieldget方法,通过传入一个实例,获得该实例的某个域的值。

我们对外部类也反编译一下,截取这个方法的部分:

  static java.lang.String access$000(top.gaolihai.test.Person);
    Code:
       0: aload_0
       1: getfield      #1                  // Field name:Ljava/lang/String;
       4: areturn

确实如此。

这样做存在安全性上的问题,当一个内部类引用了外部类私有域是,内部类为了获得访问权限,编译器会在外部类生成一个用于访问私有域的访问器。

鉴于此,在之后的 Java 版本中,内部类将被允许直接通过外部类实例的引用访问到其私有域。

我们来看一下在 Java 14 中,该类的结构和反编译结果:

"Person.java"
public class top.gaolihai.test.Person {
    
    
  // 私有域
  private java.lang.String name;
  private top.gaolihai.test.Person$Phone phone;
  // getter and setter
  public top.gaolihai.test.Person$Phone getPhone();
  public void setPhone(top.gaolihai.test.Person$Phone);
  // 默认构造
  public top.gaolihai.test.Person(java.lang.String);
}

"Person$Phone"
class top.gaolihai.test.Person$Phone {
    
    
  // 编译器添加的外部类实例引用
  final top.gaolihai.test.Person this$0;
  top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
  // 引用外部实例私有域的反汇编结果
  public java.lang.String getMasterName();
}


public class top.gaolihai.test.Person {
    
    
  public top.gaolihai.test.Person$Phone getPhone();
    Code:
       0: aload_0
       1: getfield      #1                  // Field phone:Ltop/gaolihai/test/Person$Phone;
       4: areturn

  public void setPhone(top.gaolihai.test.Person$Phone);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field phone:Ltop/gaolihai/test/Person$Phone;
       5: return

  public top.gaolihai.test.Person(java.lang.String);
    Code:
       0: aload_0
       1: invokespecial #7                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #13                 // Field name:Ljava/lang/String;
       9: return
}


class top.gaolihai.test.Person$Phone {
    
    
  final top.gaolihai.test.Person this$0;

  top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Ltop/gaolihai/test/Person;
       5: aload_0
       6: invokespecial #7                  // Method java/lang/Object."<init>":()V
       9: return

  public java.lang.String getMasterName();
    Code:
       0: aload_0
       1: getfield      #1                  // Field this$0:Ltop/gaolihai/test/Person;
       4: getfield      #13                 // Field top/gaolihai/test/Person.name:Ljava/lang/String;
       7: areturn
}

内部类对其他类的可见性

在上面的例子中,内部类是包可见的,其结构:

class top.gaolihai.test.Person$Phone {
    
    
  	// 外部类实例的引用
    final top.gaolihai.test.Person this$0;
    // 构造器
    top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
    // 访问外部类的方法
    public java.lang.String getMasterName();
}

若将其改为私有的,其结构:

class top.gaolihai.test.Person$Phone {
    
    
    // 外部类实例的引用
    final top.gaolihai.test.Person this$0;
    // 私有构造器
    private top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
    // 访问外部类的方法
    public java.lang.String getMasterName();
    // 包可见的构造器
    top.gaolihai.test.Person$Phone(top.gaolihai.test.Person, top.gaolihai.test.Person$1);
}

因为虚拟机中不存在私有类,所以该类将会被翻译为一个包可见的类,其中存在一个私有构造器和一个包可见的构造器。

当我们构造它的实例时(应该在外部类中构造,因为在源代码层面,其他类无法访问私有内部类的构造器):

person.setPhone(person.new Phone());

上面的代码被翻译为:

person.setPhone(person.new Phone(person, null));

用来调用包可见的构造器,而这个包可见的构造器将会调用私有构造器。

包可见构造器的第二个参数是什么?只是为了与私有构造器区分罢了。

这就是私有内部类的实现原理。

不过,在其后的版本中,编译器不再生成第二个构造器了。

在 Java 14 下:

class top.gaolihai.test.Person$Phone {
    
    
  final top.gaolihai.test.Person this$0;
  private top.gaolihai.test.Person$Phone(top.gaolihai.test.Person);
  public java.lang.String getMasterName();
}

局部内部类

局部内部类就是在方法局部作用域声明的内部类。

试想一下,如果一个类被声明在方法体内,那么它可能被那个作用域访问?

一定是仅在该方法局部被访问。也就是说,局部内部类就根本不需要访问说明符修饰,它的作用域被限定在声明这个内部类的块中。事实也是如此,局部内部类不能用publicprivate进行修饰。

局部内部类对外部作用域是完全隐藏的,而它不仅可以访问外部类的所有私有域,还可以访问局部作用域的变量,只不过,它被允许访问的变量必须是事实上的最终变量 (effectively final)。

又提到了这个词,事实上的最终变量 (effectively final),上一次提到他是在 lambda 表达式那里。

比局部内部类更近一步的,就是匿名内部类,或称匿名局部内部类。

搬过来前面的代码:

Arrays.sort(arr, new Comparator<Person>() {
    
    
    @Override
    public int compare(Person l, Person r) {
    
    
        return l.age - r.age;
    }
});

它的含义是:创建一个实现 Comparator 接口的类的实例,需要实现的方法 compare 定义在括号内。

如果使用局部内部类,则应该这样实现:

class Cmp implements Comparator<Person>{
    
    
    @Override
    public int compare(Person o1, Person o2) {
    
    
        return l.age - r.age;
    }
}
Arrays.sort(arr, new Cmp());

使用匿名内部类的小技巧"双括号初始化"

如果想要使用一个对象,但仅做一些简单的操作,随后便将其作为参数传入一个方法中。

例如:

ArrayList<String> friends = new ArrayList<>();
friends,add("Harry");
friends,add("Tony");
invite(friends);

可以这样改写:

inveite(new ArrayList<String>(){
     
     {
     
     add("Harry"); add("Tony");}};

嵌套在第一个大括号中的大括号是初始化块。

静态内部类

静态内部类基本上就是 C++ 中的嵌套类,当不需要在内部类中引用外部类实例时,就应该使用静态内部类。

静态内部类通过static修饰,且与普通类的行为完全相同,允许拥有静态域和静态方法。

声明在接口中的内部类自动成为staticpublic内部类。

代理 - P 258

代理用于在运行时创建一个实现了一组运行时确定的接口的类。

这块知识暂时不需要学习。

猜你喜欢

转载自blog.csdn.net/qq_16181837/article/details/112295325
今日推荐