Java内部类详解



匿名内部类

一、内部类的概述:

当一个事物的内部还存在一个需要完整结构描述的部分,且该结构仅服务于外部事物时,推荐使用内部类。在Java中,可以将⼀个类定义在另⼀个 类或者⼀个⽅法的内部,前者称为内部类,后者称为外部类。内部类也是封装的⼀种体现。

1.1 基本语法

public class OutClass {    // 外部类
    class InnerClass {      // 内部类
    }
}
// OutClass是外部类
// InnerClass是内部类

1.2 注意事项

  1. 定义在class花括号外部的类不是内部类
public class A {} 
class B {} // A和B是两个独立类

二、内部类的分类

2.1 成员内部类

public class OutClass {
    // 1.成员位置定义:未被static修饰 --->实例内部类
    public class InnerClass1{

    }
    
    // 2.成员位置定义:被static修饰 ---> 静态内部类
    static class InnerClass2{
    }
    public void method(){
    // 3.⽅法中也可以定义内部类 ---> 局部内部类:⼏乎不⽤
        class InnerClass5{
        }
    }
}
  1. 根据内部类定义的位置不同,一般分为以下几种:
类型修饰符是否有类名学习是否常用
1. 成员内部类成员位置有类名了解一般
2. 静态内部类成员位置有类名了解比较常用
3. 局部内部类方法中有类名了解少见
4. 匿名内部类方法中无类名熟练掌握最常用
2.1.1 静态内部类
public class OutClass {
    public int a;
    public static int b;
    
    static class InnerClass {
        public void methodInner() {
            // 只能访问外部类静态成员
            b = 200; 
            a = 100; // 编译错误
        }
    }
}

特点

  • 使用static修饰
  • 只能访问外部类的静态成员
  • 无需外部类实例即可创建:

为什么创建静态内部类的对象时,不需要先创建外部类对象?”

这个是因为静态内部类本质上和外部类没有“对象上的从属关系,不像普通的非静态内部类那样必须依赖外部类的实例对象。
无需先new OutClass().new InnerClass(),而是直接像下面这样:new OutClass.InnerClass();

OutClass.InnerClass inner = new OutClass.InnerClass();
2.1.2实例内部类
public class OutClass {
    public int a;
    public static int b;
    public int c = 1;
    private int d = 1;
    
    class InnerClass {
        int e = 2;
        public void methodInner() {
            d = 400;        // 访问外部类私有实例成员
            a = 100;        // 访问外部类实例成员
            b = 200;        // 访问外部类静态成员
            // 访问内部类成员
            c = 300;
            OutClass.this.c = 400; // 访问外部类同名成员
        }
    }
}

特点

  • 必须通过外部类实例创建
  • 可访问外部类所有成员,包括私有成员
  • 访问同名成员时使用外部类名.this.成员名
OutClass.InnerClass inner = new OutClass().new InnerClass();

2.2 局部内部类

定义在方法体内的内部类,使用频率极低。

public class OutClass {
    int a = 10;
    
    public void method() {
        int b = 20;
        class InnerClass {  // 局部内部类
            public void print() {
                System.out.println(a + " " + b);
            }
        }
        new InnerClass().print();
    }
}

特点

  • 仅在定义的方法体内有效
  • 不能使用访问修饰符
  • 编译后生成外部类$数字内部类.class

2.3 匿名内部类

没有类名的即时类声明,适用于一次性使用的场景。

语法格式

new 父类/接口() { 
    // 类体实现 
};

接口实现示例

interface Greeting {
    void greet();
}

public class Test {
    public static void main(String[] args) {
        Greeting greeting = new Greeting() {
            @Override
            public void greet() {
                System.out.println("Hello!");
            }
        };
        greeting.greet();
    }
}

对于匿名内部类,需要掌握其访问外部类的成员的规则,以及访问局部变量的规则

规则1:

1-匿名内部类访问外部类成员变量

1.实例方法里的匿名内部类:
可以访问外部类的所有成员变量,这点和成员内部类一样,包括实例变量,静态变量,私有变量
因为匿名内部类持有一个对外部类实例的隐式引用(OuterClass.this)所以可以访问实例成员
2.静态方法里的匿名内部类
可以访问静态成员变量,不能直接访问外部类的实例变量,如果想要访问实例变量必须通过外部实例引用。
3.访问外部类私有成员
Java编译器访问控制机制设计:内部类被视为外部类的一部分,所以可以访问私有成员

规则2:

2-变量捕获

访问匿名内部类所在的方法中的局部变量

  • 局部变量不能有默认值(必须显式初始化)

Java不会为局部变量自动赋值默认值,不像类的成员变量那样

  • 方法内部的变量不可定义为由访问修饰符修饰的,比如public int a;

因为方法内的变量属于局部变量。它的作用范围仅局限于方法体内,访问修饰符是用于类成员或者类的访问控制,不适用于局部变量。

  • 方法内部变量不能用static修饰符修饰

因为static用于修饰类级别的成员,而方法内的变量是每次调用都会新建的,不属于类,也不属于类的成员,因此不能是静态的

  • [重要]局部变量可以使用final修饰,含义是不可被重新赋值,或者是等效final,只要你不修改它的值,编译器会自动视为final修饰。

防止变量被意外修改

为什么Java要设定匿名内部类只能访问final或者等效final的局部变量呢?

public void test(){
    int x = 10;
    new Thread(()->{
        System.out.print(x);//编译通过
    }).start();
      x = 20;//编译错误 
}

Java的设计动机:

由于方法里面的变量存储在内存中的栈中,在方法结束之后就销毁了,而匿名内部类是创建在堆上的,因此里面的变量也存在在堆内存上,可能在方法运行完之后还在跑(比如在线程中)

因此Java的设计是,匿名内部类只获得了一份变量的‘’副本‘’也就是拷贝,Java编译器把x的值拷贝了一份到匿名内部类的字段里区,为了防止你后续又修改了x,会造成副本和原变量不一致,输出不可预测,引发逻辑混乱。所以Java要求不能修改被捕获的变量,必须是final或者等效final。

为什么方法结束了,线程还在运行?

因为线程是独立的执行路径,当你在方法里启动一个线程,线程会在新的执行路径(堆内存+线程栈)中继续运行,而不是随着方法结束就停掉。

捕获变量指的是:被局部内部类、匿名内部类或 Lambda 表达式访问到的、定义在方法外部(但仍是局部作用域内)的变量。

2.3.1 构造器和初始化

规则:
匿名类没有显式构造器,但可以通过 实例初始化块 初始化成员。

new Thread() {
    private String message;
    {  // 实例初始化块(类似构造器)
        message = "初始化完成";
    }
    @Override
    public void run() {
        System.out.println(message);  // 输出:初始化完成
    }
};
2.3.2 继承与实现

规则:
匿名类 只能继承一个类 或 实现一个接口,不能同时继承和实现多个

// 正确:实现一个接口
new Runnable() { 
    @Override public void run() {} 
};

// 正确:继承一个类
new Thread() { 
    @Override public void run() {} 
};

// 错误:同时继承和实现(语法不允许)
// new Thread() implements Runnable { ... }

以下是一道匿名内部类的典型题目
下面 Java 代码的运行结果为()

public class Main {
    private int num = 10;
    public static void main(String[] args) {
        Main main = new Main() {
            @Override
            public void printNum() {
                System.out.println(num);
            }
        };
        main.printNum();
    }
 
    public void printNum() {
        System.out.println(num);
    }
}

这道题考察了Java匿名内部类的语法规则和访问特性。
在该代码中存在编译错误,主要原因是在匿名内部类中访问外部类的私有成员变量num时违反了Java的内部类访问规则。

  1. 匿名内部类通过new Main(){}的方式创建,并重写了printNum()方法
  2. 在匿名内部类中的静态main方法直接访问外部类的私有成员实例变量num是不允许的
  3. 要访问外部类的私有成员,需要使用Main.this.num的形式,但由于是static方法我们仍然无法通过Main.this.num获取
  4. 另外,匿名内部类中的变量会隐藏外部类的同名变量

要修复这个编译错误,可以有以下方法:

(把 num 改为 static)

private static int num = 10; // 改为 static

这个题目很好地体现了Java内部类访问外部类成员的规则限制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值