内部类详解

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

内部类的分类

Java 中内部类分为以下四种:

  1. 成员内部类
  2. 局部内部类
  3. 静态内部类
  4. 匿名内部类

内部类对比与特点说明

公共特点说明:

  1. 可以使用外部类所有的成员变量和方法,包括私有的。
  2. 非静态内部类不能定义 static 元素。
  3. 内部类可以多层嵌套
  4. 内部类可以实现局部覆盖
  5. 创建的非静态内部类的时候,需要先实例化外部类
  6. 外部类可以通过实例化内部类访问到内部类的私有属性
  7. 内部类是可以使用外部类的注解的,例如:@Slf4j

静态内部类特点说明:

  1. 可以定义 static 元素
  2. 实例化时候不需要实例化外部类,可以直接实例化自己
  3. 不能使用外部类的非静态成员变量和方法

成员内部类

代码演示:

package com.aha.commons.innerclass;

import lombok.extern.slf4j.Slf4j;

/**
 * 内部类 - 1.成员内部类演示
 *
 * @author WT
 * date 2021/11/9
 */
@Slf4j
public class MemberInnerClassOuter {

    // 只有局部变量建议使用 基本数据类型 其他的情况包括参数一般建议使用包装类型 使用包装类型时候要注意 NPE 的风险
    private static String name = "aha";
    private Integer age = 20;

    // 成员内部类
    class MemberInnerClass {

        private Integer age = 30;
        private String innerName = "inner_aha";

        public void show () {
            log.info("成员内部类可以直接访问外部类的静态私有属性,外部类的静态私有属性name为:{}", name);
            log.info("成员内部类中访问自己和外部类同名的属性会有局部覆盖的现象,访问自己的属性age为:{}", age);
            // 访问被局部覆盖的外部类的属性的方式  MemberInnerClassOuter.this.age
            log.info("成员内部类中访问自己和外部类同名的属性会有局部覆盖的现象,访问外部类的属性age为:{}", MemberInnerClassOuter.this.age);
        }

        // 内部类的嵌套
        class InnerClass {

        }

    }

    // 外部类访问内部类的属性需要进行实例化 也可以访问内部类的私有属性
    public void showInnerName () {
        log.info("外部类访问内部类的属性需要进行实例化 也可以访问内部类的私有属性,innerName:{}", new MemberInnerClass().innerName);
    }

}

class Print {

    public static void main(String[] args) {

        MemberInnerClassOuter memberInnerClassOuter = new MemberInnerClassOuter();

        // 外部类也可以通过内部类的实例化 访问内部类的私有属性
        // 外部类访问内部类的属性需要进行实例化 也可以访问内部类的私有属性,innerName:inner_aha
        memberInnerClassOuter.showInnerName();

        // 创建非静态的内部类的话 需要先实例化外部类
        MemberInnerClassOuter.MemberInnerClass memberInnerClass = memberInnerClassOuter.new MemberInnerClass();
        // 成员内部类可以直接访问外部类的静态私有属性,外部类的静态私有属性name为:aha
        // 成员内部类中访问自己和外部类同名的属性会有局部覆盖的现象,访问自己的属性age为:30
        // 成员内部类中访问自己和外部类同名的属性会有局部覆盖的现象,访问外部类的属性age为:20
        memberInnerClass.show();

    }

}
复制代码

常见面试题:

class Outer {
    
    public int age = 18;    
    
    class Inner {
        
        public int age = 20;    
        
        public viod showAge() {
            
            int age  = 25;
            
            System.out.println();
            System.out.println();
            System.out.println();
            
        }
    }
}
复制代码

问题描述:

在三个 println() 语句中填写相应的代码,使得这三个语句的输出依次为:25,20,18

答案:

agethis.ageOuter.this.age

局部内部类

格式:

class Outer {
    public void method(){
        class Inner {
        }
    }
}
复制代码

特点:

主要是作用域发生了变化,只能在自身所在的方法中使用。

示例:

class Outer {
    
    private int age = 20;
    
    public void method() {
        final int age2 = 30;
        
        class Inner {
            public void show() {
                System.out.println(age);
                //从内部类中访问方法内变量age2,需要将变量声明为最终类型。
                System.out.println(age2);
            }
        }

        Inner i = new Inner();
        i.show();
    }
}
复制代码

注意:

这边可以看到 age2 是被 final 修饰的,局部内部类在访问局部变量的时候,局部变量在方法执行完就消亡了,而局部内部类不会,所以局部变量需要被 final 修饰,这样局部内部类就可以将使用到的局部变量给存储起来。匿名内部类也有类似的场景。

静态内部类

一般来说 static 是没有办法直接修饰类的,但是可以用来修饰内部类,这种类被称为静态内部类。

非静态内部类编译后会默认保存一个指向外部类的引用,静态内部类就没有。类似于静态属性,方法不需要外部类的实例来进行调用,所以创建静态内部类的时候也不需要实例化外部类。

class Outter {
    int age = 10;
    static age2 = 20;
    public Outter() {        
    }

    static class Inner {
        public method() {
            System.out.println(age);//错误
            System.out.println(age2);//正确
        }
    }
}

public class Test {
    public static void main(String[] args)  {
        Outter.Inner inner = new Outter.Inner();
        inner.method();
    }
}
复制代码

匿名内部类

一个没有名字的内部类,是内部类的简化写法。

格式:

new 类名或者接口名() {
    重写方法();
}
复制代码

示例:

interface Inner {
    public abstract void show();
}

class Outer {
    public void method(){
        new Inner() {
            public void show() {
                System.out.println("HelloWorld");
            }
        }.show();
    }
}

class Test {
    public static void main(String[] args)  {
        Outer o = new Outer();
        o.method();
    }
}
复制代码

这边就是使用匿名内部类创建了 Inner 接口的子类对象。

为什么要使用内部类

封装性

当在编写类的时候,遇到了一些不愿意被任何人访问的一些元素,就可以将内部类使用 private 修饰。

实现多继承 - 重点

java 是不可以多继承的,一次只能继承一个类,使用多实现来代替多继承的话,这边有一个弊端就是实现接口的时候必须要实现接口中所有的方法,有一点冗余。使用内部类就可以很好的解决多继承的问题。

public class Demo1 {
    public String name() {
        return "BWH_Steven";
    }
}

public class Demo2 {
    public String email() {
        return "xxx.@163.com";
    }
}

public class MyDemo {

    private class test1 extends Demo1 {
        public String name() {
            return super.name();
        }
    }

    private class test2 extends Demo2  {
        public String email() {
            return super.email();
        }
    }

    public String name() {
        return new test1().name();
    }

    public String email() {
        return new test2().email();
    }

    public static void main(String args[]) {
        MyDemo md = new MyDemo();
        System.out.println("我的姓名:" + md.name());
        System.out.println("我的邮箱:" + md.email());
    }
    
}
复制代码

MyDemo 中编写了两个内部类来分别继承 Demo1Demo2 这样 MyDemo 就相当于间接的继承了 Demo1Demo2 ,并且使用他们的方法或者属性。

匿名内部类实现回调

要实现回调,就要先明白什么叫做回调 ?

在计算机程序设计中,回调函数,是指通过函数参数传递到其他代码的,某一块可执行代码的引用。

通俗的来讲就是:A 类中调用 B 类中的某个方法 C,然后 B 类中方法 C 在反过来调用 A 类中的方法 D,在这里面 D 就是回调方法。

关于回调方法的详细内容可以参考:回调方法详解

代码示例:

package com.aha.commons.callback;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;

/**
 * 模拟前端调用接口组件来讲解回调的过程
 *
 * @author WT
 * date 2021/11/10
 */

interface CallbackInterface {

    // 远程调用接口之后回调的方法
    void callback(RPCResponse rpcResponse);

}

/**
 * 调用接口之后返回的对象
 */
@Data
@Accessors(chain = true)
class RPCResponse {

    private Integer code;
    private String message;
    private Map<String, Object> data;

}

/**
 * 远程调用组件
 */
@Slf4j
public class RPCModule {

    public void call (String url, Map<String,String> params, CallbackInterface callbackInterface) {
        log.info("远程调用接口地址为:{},参数为:{}",url,params);
        log.info("处理远程调用接口响应数据封装响应体...");
        HashMap<String, Object> data = new HashMap<>();
        data.put("name", "aha");
        data.put("age", 24);
        RPCResponse response = new RPCResponse().setCode(200).setMessage("请求接口成功").setData(data);
        log.info("调用接口完成,执行回调方法...");
        // 重点: 这边调用的是子类实现的 callback 方法
        callbackInterface.callback(response);
    }

}

@Slf4j
class CustomFrontPage {

    public static void main(String[] args) {
        HashMap<String, String> params = new HashMap<>();
        params.put("name", "aha");
        // 模拟前端远程调用的过程 - 查询名字叫 aha 的用户
        new RPCModule().call("/api/users", params, res -> {
            // 重点: 这边是 RPCModule 调用上游获取响应信息之后, 回调这个方法的, res 之所以有值是 RPCModule 调用时传递过来的
            if (res.getCode().equals(200)) {
                log.info("请求后端接口成功,执行成功回调,获得的响应信息为:{},获得的响应数据为:{}", res.getMessage(), res.getData());
            } else {
                log.error("请求后端接口成功,执行失败回调,错误的响应码为:{}", res.getCode());
            }
        });
    }

}
复制代码

解决继承父类和实现接口出现同名方法的问题

问题演示:

package com.aha.commons.innerclass;

import lombok.extern.slf4j.Slf4j;

interface ConflictInterface {

    void conflict();

}

@Slf4j
class ConflictClass {

    public void conflict() {
        log.info("父类的conflict方法");
    }

}

@Slf4j
public class MethodConflict extends ConflictClass implements ConflictInterface{

    // 这样就不知道 这个是覆盖谁的方法 是父类的? 还是接口的?
    @Override
    public void conflict() {
        super.conflict();
        log.info("子类的conflict方法");
    }

    public static void main(String[] args) {
        new MethodConflict().conflict();
    }

}
复制代码

解决办法说明:

package com.aha.commons.innerclass;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class MethodConflict02 extends ConflictClass {

    // 这样就明确的知道这个方法是父类的
    @Override
    public void conflict() {
        super.conflict();
        log.info("子类的conflict方法");
    }

    class ConflictInnerClass implements ConflictInterface {

        // 实现接口的方法
        @Override
        public void conflict() {
            log.info("接口的conflict方法");
        }

    }

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

    public static void main(String[] args) {
        MethodConflict02 mc = new MethodConflict02();
        // 调用父类的
        mc.conflict();
        // 调用实现接口的
        mc.getInner().conflict();
    }

}
复制代码