java的内部类详解

目录

  • 内部类概述
  • 内部类的分类
  • 非静态内部类
    • 定义非静态内部类
    • 非静态内部类与static成员
    • 非静态内部类与外部类成员交互
    • 非静态内部类成员与外部类成员同名问题
  • 静态内部类
    • 静态内部类与外部类的交互
    • 静态内部类与外部接口
  • 局部内部类
    • 局部内部类与外界交互
  • 外界使用内部类
  • 内部类与继承
  • 内部类与class文件
  • 内部类与访问控制修饰符
  • 匿名内部类
    • 匿名内部类与外部类交互
    • 匿名内部类的应用场景
    • 匿名(局部)内部类与局部变量



内部类概述

在一个类内部的类就是内部类(或者叫嵌套类),包含内部类的类被称为外部类(也叫宿主类)。
内部类比较常见的是匿名内部类,其他内部类其实实际开发很少使用。
掌握内部类需要对内部类进行分类,并且了解不同内部类在外部类的内部访问方式和在外界的访问方式。



内部类的分类

可以把内部类分为以下几类:

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

本篇博文会依次介绍不同的内部类的用法细节。



非静态内部类

定义非静态内部类

语法如下
[访问控制修饰符] class 内部类名 {类体}

可以看到,非静态内部类的语法格式和普通类差不多,核心区别是内部类放在外部类的内部,而普通类是单独作为一个程序单元独立存在。

非静态内部类是外部类的一个实例成员,和实例变量、实例方法、初始化块、构造器的地位差不多。

非静态内部类与static成员

非静态内部类里面不能包含static成员,包括静态代码块、类变量、类方法。既然是非静态内部类,就表明里面的成员都是对象相关的。
想要在内部类中定义static成员,就只能使用静态内部类。

非静态内部类与外部类成员交互

非静态内部类可以直接访问外部类的私有实例成员,比如private修饰的实例变量或者私有的实例方法。
这是因为内部类虽然是类,但它在外部类的内部,地位和其他实例成员一样的。而在外部类的内部,所有的实例成员是可以任意交互的(哪怕是最小的private权限)。
要想创建内部类的对象,必须先创建对应的外部类对象在内部类对象创建完之后,会保存一份它所寄生的外部类对象的引用(也就是外部类名.this)。这个引用可以帮助内部类对象直接访问外部类对象的私有成员。

反过来,外部类想访问非静态内部类的实例成员,必须先创建非静态内部类的对象
这其实很好理解,因为如果存在内部类对象,必然有与之对应的外部类对象。
但有外部类对象不代表已经创建了内部类对象,如果根本不存在内部类对象,直接访问其实例成员,当然会报错。

而且,外部类的静态成员(比如类方法)不能访问非静态内部类,比如创建非静态内部类的对象。因为非静态内部类对象也是实例成员,静态成员无法访问实例成员。

非静态内部类成员与外部类成员同名问题

如果非静态内部类的成员和外部类的成员同名。比如有同名的变量或者同名的方法,都遵从就近原则

比如在非静态内部类的方法里访问某个变量时,会优先查找是否存在同名的局部变量,如果不存在,则查找内部类的同名实例变量,如果不存在,才会查找外部类是否存在同名的成员变量,如果还不存在,则报错。

如果想绕开就近原则,就必须使用this关键字:

  1. 如果想绕过局部变量,访问内部类的实例变量,直接使用this.变量名来访问。
  2. 如果想绕过内部类的同名变量,直接访问外部类的成员变量,需要使用外部类类名.this作为限定来区分。表明这个变量是外部类拥有的。


静态内部类

用static修饰的内部类就是静态内部类。和非静态内部类不同,静态内部类是属于外部类本身,而不是属于外部类对象。
静态内部类里面可以有静态成员和非静态成员,而非静态内部类不能有静态成员。

静态内部类与外部类的交互

静态内部类不能访问外部类的实例成员,但可以直接访问外部类的类成员,因为需要满足静态成员不能访问非静态成员的规则。
其实很简单,静态内部类是与外部类本身联系的,它没有保存任何关于外部类对象的信息,自然不能访问外部类对象相关的实例成员。

外部类要想访问静态内部类的类成员,需要用类名.类成员的方式访问。
外部类要想访问静态内部类的实例成员,需要创建静态内部类的对象,用引用变量名.类成员的方式访问。

静态内部类与外部接口

接口里也可以有内部类,接口的内部类默认是public static访问权限,所以接口的内部类只能是公开的静态内部类



局部内部类

局部内部类是一个很鸡肋的概念,因为局部内部类只在方法内有效,它的地位很像局部变量。所以局部内部类没有访问修饰符,也不能用static修饰,但是可以用final修饰,表示在方法内不能创建局部内部类的子类。
局部内部类举例:

public class LocalInnerClass {
    public static void main(String[] args) {
        class InBase {
            int a = 10;
        }
        class InSub extends InBase {
            int b = 20;
        }
        var is = new InSub();
        System.out.println(is.a + "," + is.b);
    }
}

局部内部类的创建、使用和定义子类都必须在方法内部进行,范围实在太小了,所以复用性很差,实际开发基本不用。

局部内部类与外界交互

局部内部类的地位和局部变量差不多,它们都在某个方法内部,而类中的方法可以访问类的其他成员(当然静态方法不能访问非静态成员),所以局部内部类由于在方法内部,也可以自由访问类中的其他成员。只是在静态方法中的局部内部类不能访问非静态成员



外界使用内部类

如果是在外部类的内部使用内部类,其实和使用普通类大同小异,前面已经讲过了。
但在外界使用内部类时,创建类的格式和普通类不一样:

  1. 在外部类以外使用非静态内部类
    首先非静态内部类的访问权限必须允许外界访问
    定义的语法格式
    OuterClass.InnerClass 引用变量名 = 外部类对象引用名.new InnerClass(参数列表);

从上面可以看到,内部类的类型在外界是OuterClass.InnerClass,必须加上外部类类名的前缀,才能区分是哪一个外部类里的内部类(如果外部类有包名,还需要加上完整的包名)。
在创建非静态内部类对象时,必须先创建外部类对象因为非静态内部类对象是寄生在外部类对象里的。所以需要在new关键字前加上外部类对象的引用名。

2.在外部类以外使用静态内部类
首先静态内部类的访问权限必须允许外界访问
定义的语法格式
OuterClass.InnerClass 引用变量名 = new OuterClass.InnerClass(参数列表);

创建静态内部类的对象比较简单,因为不需要绑定外部类的对象,只需要借助外部类本身。所以只需要new 外部类类名.内部类类名(参数列表)即可。

可以看出,不管是静态内部类还是非静态内部类,它们的引用变量名格式都为:OuterClass.InnerClass。只需要外部类类名的前缀就可以区分到底是哪个外部类的内部类。



内部类与继承

内部类在外界也可以有子类,而且内部类的子类可以是一个外部类。
子类继承父类需要调用父类的构造器,而内部父类的构造器调用需要依靠外部类非静态内部类的构造器调用需要借助外部类的对象,静态内部类的调用需要借助外部类本身

以创建非静态内部类的子类举例:

class Out {
	class In {
	}
}

public class SubClass extends Out.In {
	public SubClass(Out out) {
		//想要调用内部类的父类构造器,必须要提供外部类的对象
		out.super();
	}
}

以创建静态内部类的子类举例:

class StaticOut {
    static class StaticIn {
        public StaticIn(int i) {}
    }
}

public class StaticSubClass extends StaticOut.StaticIn{
    public StaticSubClass() {
    	// 调用静态内部类父类的构造器不需要借助外部类的对象,直接调用super语句即可
        super(10);
    }
}

注意:子类可以继承外部类的非静态内部类,但不能重写
因为非静态内部类是外部类对象的实例成员,所以可以被子类对象继承。
但是,由于内部类的全称需要外部类的前缀,即使子类有和外部父类同名的内部类,它们的前缀也是不同的,不满足重写的规则。
所以,子类只是可以继承父类的非静态内部类,如果定义了同名的内部类,只能算新定义了一个内部类。



内部类与class文件

编译结束后,内部类也会生成.class文件,不过在格式上和普通类有区别。
格式为:

  1. 成员内部类:外部类类名$内部类类名.class
    之所以要加上外部类类名的前缀,是因为可能不同的外部类拥有同名的内部类,需要用这种方式区分。
  2. 局部内部类:外部类类名$N内部类类名.class
    对于局部内部类,可能同一个外部类中不同方法拥有同名局部内部类,所以需要用一个数字N来区分是哪一个方法。


内部类与访问控制修饰符

由于成员内部类(包括静态内部类和非静态内部类)在外部类的内部,所以内部类的访问权限有4种:private、default(也就是省略访问修饰符)、protected、public。
而局部内部类和匿名内部类的访问权限在方法内部,和局部变量的地位差不多,也就不需要访问控制修饰符修饰。

如果用private修饰内部类,表示需要把内部类封装在外部类内部,它只希望外部类自身访问,不希望暴露给外界访问。
比如Cow类有一个CowLeg的非静态内部类,由于奶牛的腿脱离奶牛没有任何意义,所以CowLeg类应该用private修饰。

如果用更高权限修饰内部类,则表示不仅希望外部类自身使用内部类,也希望外界可以直接创建内部类。



匿名内部类

匿名内部类是内部类最常用的概念,它的创建是一次性的,连类名都没有。
创建匿名内部类时会立刻得到一个该类的实例,之后类的定义立即消失,无法重复使用

匿名内部类必须继承一个父类或者实现一个接口,而且只能实现一个接口,不能使用多继承。

定义匿名内部类的语法如下
new 实现接口() | 父类构造器(实参列表)
{
类体
}

由于匿名内部类必须在定义时立刻创建一次性的实例,所以匿名内部类不能是抽象类。这意味着匿名内部类必须实现抽象父类或者接口的所有抽象方法。
而且匿名内部类不能有构造器,因为它根本没有类名。匿名内部类初始化的工作需要依赖初始化块和定义时设置默认值。

对于实现接口的匿名内部类,在初始化对象时会调用自身隐藏的默认构造器(空的无参构造器),对于继承父类的匿名内部类,在初始化对象时会拥有一个和父类相同参数类型的构造器,这个构造器会调用父类的构造器,根据实参列表去匹配父类的某个构造器。

匿名内部类与外部类交互

外部类只能在某个方法内部使用匿名内部类,但是匿名内部类可以直接访问外部类的成员,如果是在类方法中的匿名内部类,只能访问静态成员,如果在实例方法中的匿名内部类,可以访问所有成员。这其实很好理解,外部类的成员之间本来就可以互相访问,匿名内部类在方法中,自然可以随意访问外部类的其他成员。
和匿名内部类一样,局部内部类自然也可以做类似的事情。因为局部内部类也是方法中的代码,也可以直接访问外部类的其他成员(根据所在方法是否是类方法决定是否能访问实例成员)。

匿名内部类的应用场景

如果需要一次性的对象,匿名内部类就更加合适,因为它的定义相比定义一个独立的类更加简洁。
但如果类需要实现的方法过多,就不要用匿名内部类,这样会显得代码很臃肿。

很常见的场景是对于参数是接口类型的方法,需要提供一个一次性的实现类实例。

匿名(局部)内部类与局部变量

匿名(局部)内部类内部可以访问方法中的局部变量,在java8以前,被匿名(局部)内部类访问的局部变量必须用final标记。但java8以后可以省略final,系统会自动帮你标记final。
所以,如果匿名(局部)内部类内部使用了方法的局部变量,就不能对该局部变量再进行赋值。

你可能感兴趣的:(javaSE,java,开发语言)